本文為轉載,來自
前面我們不止一次的提到,Java是一種跨平台的語言,為什么可以跨平台,因為我們編譯的結果是中間代碼—字節碼,而不是機器碼,那字節碼在整個Java平台扮演着什么樣的角色的呢?JDK1.2之前對應的結構圖如下所示:
從JDK1.2開始,迫於Java運行始終筆C++慢的壓力,JVM的結構也慢慢發生了一些變化,JVM在某些場景下可以操作一定的硬件平台,一些核心的Java庫甚至也可以操作底層的硬件平台,從而大大提升了Java的執行效率,在前面JVM內存模型和垃圾回收中也給大家演示了如何操作物理內存,下圖展示了JDK1.2之后的JVM結構模型。
那C++和Java在編譯和運行時到底有啥不一樣?為啥Java就能跨平台的呢?
我們從上圖可以看出。
C++發布的就是機器指令,而Java發布的是字節碼,字節碼在運行時通過JVM做一次轉換生成機器指令,因此能夠更好的跨平台運行。如圖所示,展示了對應代碼從編譯到執行的一個效果圖。
我們知道JVM是基於棧執行的,每個線程會建立一個操作棧,每個棧又包含了若干個棧幀,每個棧幀包含了局部變量、操作數棧、動態連接、方法的返回地址信息等。其實在我們編譯的時候,需要多大的局部變量表、操作數深度等已經確定並寫入了Code屬性,因此運行時內存消耗的大小在啟動時已經已知。
在棧幀中,最小的單位為變量槽(Variable Slot),其中每個Slot占用32個字節。在32bit的JVM中32位的數據類型占用1個Slot,64bit數據占用2個Slot;在64bit中使用64bit字節填充來模擬32bit(又稱補位),因此我們可以得出結論:64bit的JVM比32bit的更消耗內存,但是又出32bit機器的內存上限限制,有時候犧牲一部分還是值得的。Java的基本數據類型中,除了long、double兩種數據類型為64bit以外,boolean、byte、char、int、float、reference等都是32bit的數據類型。
在棧幀中,局部變量表中的Slot是可以復用的,如在一個方法返回給上一個方法是就可以通過公用Slot的方法來節約內存控件,但這在一定程度省會影響垃圾回收,因此JVM不確定這塊Slot空間是否還需要復用。
Slot復用會給JVM的垃圾回收帶來一定影響,如下代碼:
1 package com.yhj.jvm.byteCode.slotFree; 2 3 /** 4 5 * @Described:Slot局部變量表 沒有破壞GCRoot情況演示 6 7 * @VM params :-XX:+PrintGCDetails -verbose:gc 8 9 * @author YHJ create at 2012-2-22 下午04:37:29 10 11 * @FileNmae com.yhj.jvm.byteCode.slotFree.SlotFreeTestCase.java 12 13 */ 14 15 public class SlotFreeTestCase { 16 17 /** 18 19 * @param args 20 21 * @Author YHJ create at 2012-2-22 下午04:37:25 22 23 */ 24 25 @SuppressWarnings("unused") 26 27 public static void main(String[] args) { 28 29 //case 1 30 31 byte[] testCase = new byte[10*1024*1024]; 32 33 System.gc(); 34 35 // //case 2 36 37 // { 38 39 // byte[] testCase = new byte[10*1024*1024]; 40 41 // } 42 43 // System.gc(); 44 45 // //case 3 46 47 // { 48 49 // byte[] testCase = new byte[10*1024*1024]; 50 51 // } 52 53 // int a = 0; 54 55 // System.gc(); 56 57 // //case 5 58 59 // byte[] testCase = new byte[10*1024*1024]; 60 61 // testCase=null; 62 63 // System.gc(); 64 65 } 66 }
如上所示,當我們執行這段代碼的時候並不會引發GC的回收,因為很簡單,我的testCase對象還在使用中,生命周期並未結束,因此運行結果如下
但是我們換下面的case2這種寫法呢?
1 //case 2 2 3 { 4 5 byte[] testCase = new byte[10*1024*1024]; 6 7 } 8 9 System.gc();
這種寫法,testCase在大括號中生命周期已經結束了,會不會引發GC的呢?我們來看結果:
1 //case 3 2 3 { 4 5 byte[] testCase = new byte[10*1024*1024]; 6 7 } 8 9 int a = 0; 10 11 System.gc();
這下我們貌似看到奇跡了
沒錯,
JVM做了回收操作,因為JVM在做下面的操作時並沒有發現公用的Slot,因此該內存區域被回收。但是我們這樣寫代碼會讓很多人感到迷惑,我們應該怎樣寫才能更好一點讓人理解的呢?
1 //case 5 2 3 byte[] testCase = new byte[10*1024*1024]; 4 5 testCase=null; 6 7 System.gc();
無疑,這樣寫才是最好的,這也是書本effective Java中強調了很多遍的寫法,隨手置空不用的對象。
我們知道private int a;這么一個語句在一個類中的話他的默認值是0,那么如果是在局部變量中的呢?我們開看這樣一段代碼:
1 package com.yhj.jvm.byteCode.localVariableInit; 2 3 /** 4 5 * @Described:局部變量拒絕默認初始化 6 7 * @author YHJ create at 2012-2-24 下午08:40:34 8 9 * @FileNmae com.yhj.jvm.byteCode.localVariableInit.LocalVariableInit.java 10 11 */ 12 13 public class LocalVariableInit { 14 15 16 17 /** 18 19 * @param args 20 21 * @Author YHJ create at 2012-2-22 下午05:12:06 22 23 */ 24 25 @SuppressWarnings("unused") 26 27 public static void main(String[] args) { 28 29 int a; 30 31 System.out.println(a); 32 33 } 34 35 }
這段代碼的運營結果又是什么的呢?
很多人會回答0.我們來看一下運行結果:
沒錯,就是報錯了,如果你使用的是
Eclipse這種高級一點的IDE的 話,在編譯階段他就會提示你,該變量沒有初始化。原因是什么的呢?原因就是,局部變量並沒有類實例變量那樣的連接過程,前面我們說過,類的加載分為加載、 連接、初始化三個階段,其中連接氛圍驗證、准備、解析三個階段,而驗證是確保類加載的正確性、准備是為類的靜態變量分配內存,並初始化為默認值、解析是把 類中的符號引用轉換為直接引用。而外面的初始化為類的靜態變量賦值為正確的值。而局部變量並沒有連接的階段,因此沒有賦值為默認值這一階段,因此必須自己 初始化才能使用。
我們在類的加載中提到類的靜態連接過程,但是還有一部分類是需要動態連接的,其中以下是需要動態連接的對象
1、 實例變量(類的變量或者局部變量)
2、 通過其他榮報告期動態注入的變量(IOC)
3、 通過代碼注入的對象(void setObj(Object obj))
所有的動態連接都只有准備和解析階段,沒有再次校驗(校驗發生在連接前類的加載階段),其中局部變量不會再次引發准備階段。
前面我們提到JVM的生命周期,在以下四種情況下會引發JVM的生命周期結束
1、 執行了System.exit()方法
2、 程序正常運行結束
3、 程序在執行過程中遇到了異常或者錯誤導致異常終止
4、 由於操作系統出現錯誤而導致JVM進程終止
同樣,在以下情況下會導致一個方法調用結束
1、 執行引擎遇到了方法返回的字節碼指令
2、 執行引擎在執行過程中遇到了未在該方法內捕獲的異常
這時候很多人會有一個疑問:當程序返回之后它怎么知道繼續在哪里執行?這就用到了我們JVM內存模型中提到了的PC計數器。方法退出相當於當前棧出棧,出棧后主要做了以下事情:
1、 回復上層方法的局部變量表
2、 如果有返回值的話將返回值壓入到上層操作數棧
3、 調整PC計數器指向下一條指令
除了以上信息以外,棧幀中還有一些附加信息,如預留一部分內存用於實現一些特殊的功能,如調試信息,遠程監控等信息。
接下來我們要說的是方法調用,方法調用並不等於方法執行,方法調用的任務是確定調用方法的版本(調用哪一個方法),在實際過程中有可能發生在加載期間也有可能發生在運行期。Class的編譯過程並不包含類似C++的連接過程,只有在類的加載或者運行期才將對應的符號引用修正為真正的直接引用,大大的提升了Java的靈活性,但是也大大增加了Java的復雜性。
在類加載的第二階段連接的第三階段解析,這一部分是在編譯時就確定下來的,屬於編譯期可知運行期不可變。在字節碼中主要包含以下兩種
1、 invokestatic 主要用於調用靜態方法,屬於綁定類的調用
2、 invokespecial 主要用於調用私有方法,外部不可訪問,綁定實例對象
還有一種是在運行時候解析的,只有在運行時才能確定下來的,主要包含以下兩方面
1、 invokevirtual 調用虛方法,不確定調用那一個實現類
2、 invokeinterface 調用接口方法,不確定調用哪一個實現類
我們可以通過javap的命令查看對應的字節碼文件方法調用的方式,如下圖所示
Java
方法在調用過程中,把invokestatic和invokespecial定義為非虛方法的調用,非虛方法的調用都是在編譯器已經確定具體要調用哪一個方法,在類的加載階段就完成了符號引用到直接引用的轉換。除了非虛方法以外,還有一種被final修飾的方法,因被final修飾以后調用無法通過其他版本來覆蓋,因此被final修飾的方法也是在編譯的時候就已知的廢墟方法。
除了解析,Java中還有一個概念叫分派,分派是多態的最基本表現形式,可分為單分派、多分派兩種;同時分派又可以分為靜態分派和動態分派,因此一組合,可以有四種組合方式。其實最本質的體現就是方法的重載和重寫。我們來看一個例子
1 package com.yhj.jvm.byteCode.staticDispatch; 2 3 /** 4 5 * @Described:靜態分配 6 7 * @author YHJ create at 2012-2-24 下午08:20:06 8 9 * @FileNmae com.yhj.jvm.byteCode.staticDispatch.StaticDispatch.java 10 11 */ 12 13 public class StaticDispatch { 14 15 16 17 static abstract class Human{}; 18 19 static class Man extends Human{} ; 20 21 static class Woman extends Human{} ; 22 23 24 25 public void say(Human human) { 26 27 System.out.println("hi,you are a good human!"); 28 29 } 30 31 public void say(Man human) { 32 33 System.out.println("hi,gentleman!"); 34 35 } 36 37 public void say(Woman human) { 38 39 System.out.println("hi,yong lady!"); 40 41 } 42 43 /** 44 45 * @param args 46 47 * @Author YHJ create at 2012-2-24 下午08:20:00 48 49 */ 50 51 public static void main(String[] args) { 52 53 Human man = new Man(); 54 55 Human woman = new Woman(); 56 57 StaticDispatch dispatch = new StaticDispatch(); 58 59 dispatch.say(man); 60 61 dispatch.say(woman); 62 63 } 64 65 }
這個例子的執行結果會是什么呢?我們來看一下結果
和你的預期一致么?這個其實是一個靜態分派的杯具,
man和woman兩個對象被轉型以后,通過特征簽名匹配,只能匹配到對應的父類的重載方法,因此導致最終的結構都是執行父類的代碼。因為具體的類是在運行期才知道具體是什么類型,而編譯器只確定是Human這種類型的數據。
這種寫法曾經在我們項目中也發生過一次。如下代碼所示
1 package com.yhj.jvm.byteCode.staticDispatch; 2 3 import java.util.ArrayList; 4 5 import java.util.List; 6 7 /** 8 9 * @Described:蝌蚪網曾經的杯具 10 11 * @author YHJ create at 2012-2-26 下午09:43:20 12 13 * @FileNmae com.yhj.jvm.byteCode.staticDispatch.CothurnusInPassport.java 14 15 */ 16 17 public class CothurnusInPassport { 18 19 /** 20 21 * 主函數入口 22 23 * @param args 24 25 * @Author YHJ create at 2012-2-26 下午09:48:02 26 27 */ 28 29 public static void main(String[] args) { 30 31 List<CothurnusInPassport> inPassports = new ArrayList<CothurnusInPassport>(); 32 33 inPassports.add(new CothurnusInPassport()); 34 35 String xml = XML_Util.createXML(inPassports); 36 37 System.out.println(xml); 38 39 } 40 41 } 42 43 class XML_Util{ 44 45 public static String createXML(Object obj){ 46 47 return 。。。// ... 通過反射遍歷屬性 生成對應的XML節點 48 49 } 50 51 public static String createXML(List<Object> objs){ 52 53 StringBuilder sb = new StringBuilder(); 54 55 for(Object obj : objs) 56 57 sb.append(createXML(obj)); 58 59 return new String(sb); 60 61 } 62 63 }
當時我們項目組寫了以惡搞XML_Util的一個類用於生成各種XML數據,其中一個實例傳入的參數是Object,一個是一個List類型的數據,如上面代碼所示,我的調用結果會執行哪一個的呢?結果大家已經很清楚了,他調用了createXML(Object obj)這個方法,因此生成過程中老是報錯,原因很簡單,就是因為我叼用的時候泛型 不匹配,進行了隱式的類型轉換,因此無法匹配到對應的List《Object》最終調用了createXML(Object obj)這個方法。
下面我們來看一道惡心的面試題,代碼如下:
1 package com.yhj.jvm.byteCode.polymorphic; 2 3 import java.io.Serializable; 4 5 /** 6 7 * @Described:重載測試 8 9 * @author YHJ create at 2012-2-24 下午08:41:12 10 11 * @FileNmae com.yhj.jvm.byteCode.polymorphic.OverLoadTestCase.java 12 13 */ 14 15 public class OverLoadTestCase { 16 17 public static void say(Object obj){ System.out.println("Object"); } 18 19 public static void say(char obj){ System.out.println("char"); } 20 21 public static void say(int obj){ System.out.println("int"); } 22 23 public static void say(long obj){ System.out.println("long"); } 24 25 public static void say(float obj){ System.out.println("float"); } 26 27 public static void say(double obj){ System.out.println("double"); } 28 29 public static void say(Character obj){ System.out.println("Character"); } 30 31 public static void say(Serializable obj){ System.out.println("Serializable"); } 32 33 public static void say(char... obj){ System.out.println("char..."); } 34 35 public static void main(String[] args) { 36 37 OverLoadTestCase.say('a'); 38 39 } 40 41 }
這樣的代碼會執行什么呢?這個很簡單的了,是char,那如果我注釋掉char這個方法,再執行呢?是int,繼續注釋,接下來是什么的呢?大家可以自己測試一下,你會發現這段代碼有多么的惡心。
我們接下來再看一段代碼:
1 package com.yhj.jvm.byteCode.dynamicDispatch; 2 3 /** 4 5 * @Described:動態分派測試 6 7 * @author YHJ create at 2012-2-26 下午10:05:43 8 9 * @FileNmae com.yhj.jvm.byteCode.dynamicDispatch.DynamicDispatch.java 10 11 */ 12 13 public class DynamicDispatch { 14 15 static abstract class Human{ 16 17 public abstract void say(); 18 19 }; 20 21 static class Man extends Human{ 22 23 @Override 24 25 public void say(){ 26 27 System.out.println("hi,you are a good man!"); 28 29 } 30 31 } ; 32 33 static class Woman extends Human{ 34 35 @Override 36 37 public void say(){ 38 39 System.out.println("hi,young lady!"); 40 41 } 42 43 } ; 44 45 //主函數入口 46 47 public static void main(String[] args) { 48 49 Human man = new Man(); 50 51 Human woman = new Woman(); 52 53 man.say(); 54 55 woman.say(); 56 57 woman = new Man(); 58 59 woman.say(); 60 61 } 62 63 }
這段代碼執行的結果會是什么的呢?這個不用說了吧?企業級的應用經常會使用這些的方法重寫,這是動態分配的一個具體體現,也就是說只有運行期才知道具體執行的是哪一個類,在編譯期前並不知道會調用哪一個類的這個方法執行。
我們再來看一段代碼,這段代碼被稱為“一個艱難的決定”
1 //動態單分派靜態多分派 宗量選擇 2 3 package com.yhj.jvm.byteCode.dynamicOneStaticMoreDispatch; 4 5 /** 6 7 * @Described:一個艱難的決定 8 9 * @author YHJ create at 2012-2-24 下午09:23:26 10 11 * @FileNmae com.yhj.jvm.byteCode.dynamicOneStaticMore.OneHardMind.java 12 13 */ 14 15 public class OneHardMind { 16 static class QQ{} //騰訊QQ 17 18 static class _360{} //360安全衛士 19 20 static class QQ2011 extends QQ{} //騰訊QQ2011 21 22 static class QQ2012 extends QQ{} //騰訊QQ2012 23 24 //百度 25 26 static class BaiDu{ 27 28 public static void choose(QQ qq){ System.out.println("BaiDu choose QQ"); } 29 30 public static void choose(QQ2011 qq){ System.out.println("BaiDu choose QQ2011"); } 31 32 public static void choose(QQ2012 qq){ System.out.println("BaiDu choose QQ2012"); } 33 34 public static void choose(_360 _){ System.out.println("BaiDu choose 360 safe"); } 35 36 } 37 38 //迅雷 39 40 static class Thunder{ 41 42 public static void choose(QQ qq){ System.out.println("Thunder choose QQ"); } 43 44 public static void choose(QQ2011 qq){ System.out.println("Thunder choose QQ2011"); } 45 46 public static void choose(QQ2012 qq){ System.out.println("Thunder choose QQ2012"); } 47 48 public static void choose(_360 qq){ System.out.println("Thunder choose 360 safe"); } 49 50 } 51 52 //主函數入口 53 54 @SuppressWarnings("static-access") 55 56 public static void main(String[] args) { 57 58 BaiDu baiDu = new BaiDu(); 59 60 Thunder thunder = new Thunder(); 61 62 QQ qq = new QQ(); 63 64 _360 _360_safe = new _360(); 65 66 baiDu.choose(qq); 67 68 thunder.choose(_360_safe); 69 70 qq = new QQ2011(); 71 72 baiDu.choose(qq); 73 74 qq = new QQ2012(); 75 76 baiDu.choose(qq); 77 78 } 79 }
這段代碼的執行結果又是什么?現在可以很簡單的說出對應的結果了吧!
從這個例子我們可以看出,
Java是靜態多分派動態單分派的 同理,C#3.0 、C++也是靜態多分配,動態單分派的 C#4.0后引入類型dynamic可以實現動態多分派,sun公司在JSR-292中提出了動態多分派的實現,規划在JDK1.7推出,但是被oracle收購后,截至目前,JDK1.7已經不發布了多個版本,但尚未實現動態多分派。至於動態多分派究竟是怎么樣子的?我們可以參考Python的多分派實例。
那虛擬機為什么能夠實現不同的類加載不同的方法,什么時候使用靜態分派?什么時候又使用動態分派呢?我們把上面的示例用一個圖來表示,大家就很清楚了!
當 子類有重寫父類的方法時,在系統進行解析的時候,子類沒有重寫的方法則將對應的符號引用解析為父類的方法的直接引用,否則解析為自己的直接引用,因此重寫 永遠都會指向自己的直接引用,但是重載在解析時並不知道具體的直接引用對象是哪一個?所以只能解析為對應的表象類型的方法。
我們在前面已經提到,新的Java編譯器已經不會純粹的走解釋執行之路,在一些情況下還會走編譯之路。如下圖所示:
我們知道,程序之所以能運行,是因為有指令集,而
JVM主要是基於棧的一個指令集,而還有一部分程序是基於寄存器的指令集,兩者有什么區別的呢?
基 於棧的指令集有接入簡單、硬件無關性、代碼緊湊、棧上分配無需考慮物理的空間分配等優勢,但是由於相同的操作需要更多的出入棧操作,因此消耗的內存更大。 而基於寄存器的指令集最大的好處就是指令少,速度快,但是操作相對繁瑣。下面我們來看一段代碼,看一下同樣一段代碼在不同引擎下的執行效果有啥不同。
1 public class Demo { 2 3 public static void foo() { 4 5 int a = 1; 6 7 int b = 2; 8 9 int c = (a + b) * 5; 10 11 } 12 13 }
在Client/Server VM的模式下,我們可以使用javap –verbose ${ClassName}的方式來查看對應的字節碼,而基於java的DalvikVM亦可以通過platforms\android-1.6\tools目錄中的dx工具查看對應的字節碼。具體命令為dx --dex –verbose --dump-to=packageName --dump-method=Demo.foo --verbose-dump Demo.class 。
基於棧的Hotspot的執行過程如下:
基於棧的
DalvikVM執行過程如下所示:
而基於匯編語言的展示就是這樣的了
附:基於JVM的邏輯運算模型如下圖所示
因此執行到
JVM上的過程就是下面的形式