Java之深刻JVM(6) - 字節碼執行引擎(轉)

本文爲轉載,來自java

 前面咱們不止一次的提到,Java是一種跨平臺的語言,爲何能夠跨平臺,由於咱們編譯的結果是中間代碼字節碼,而不是機器碼,那字節碼在整個Java平臺扮演着什麼樣的角色的呢?JDK1.2以前對應的結構圖以下所示:linux

JDK1.2開始,迫於Java運行始終筆C++慢的壓力,JVM的結構也慢慢發生了一些變化,JVM在某些場景下能夠操做必定的硬件平臺,一些核心的Java庫甚至也能夠操做底層的硬件平臺,從而大大提高了Java的執行效率,在前面JVM內存模型和垃圾回收中也給你們演示瞭如何操做物理內存,下圖展現了JDK1.2以後的JVM結構模型。android

C++Java在編譯和運行時到底有啥不同?爲啥Java就能跨平臺的呢?面試

 咱們從上圖能夠看出。安全

C++發佈的就是機器指令,而Java發佈的是字節碼,字節碼在運行時經過JVM作一次轉換生成機器指令,所以可以更好的跨平臺運行。如圖所示,展現了對應代碼從編譯到執行的一個效果圖。oracle

 咱們知道JVM是基於棧執行的,每一個線程會創建一個操做棧,每一個棧又包含了若干個棧幀,每一個棧幀包含了局部變量、操做數棧、動態鏈接、方法的返回地址信息等。其實在咱們編譯的時候,須要多大的局部變量表、操做數深度等已經肯定並寫入了Code屬性,所以運行時內存消耗的大小在啓動時已經已知。app

在棧幀中,最小的單位爲變量槽(Variable Slot),其中每一個Slot佔用32個字節。在32bitJVM32位的數據類型佔用1Slot64bit數據佔用2Slot;在64bit中使用64bit字節填充來模擬32bit(又稱補位),所以咱們能夠得出結論:64bitJVM32bit的更消耗內存,可是又出32bit機器的內存上限限制,有時候犧牲一部分仍是值得的。Java的基本數據類型中,除了longdouble兩種數據類型爲64bit之外,booleanbytecharintfloatreference等都是32bit的數據類型。jvm

在棧幀中,局部變量表中的Slot是能夠複用的,如在一個方法返回給上一個方法是就能夠經過公用Slot的方法來節約內存控件,但這在必定程度省會影響垃圾回收,所以JVM不肯定這塊Slot空間是否還須要複用。ide

 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的 話,在編譯階段他就會提示你,該變量沒有初始化。緣由是什麼的呢?緣由就是,局部變量並無類實例變量那樣的鏈接過程,前面咱們說過,類的加載分爲加載、 鏈接、初始化三個階段,其中鏈接氛圍驗證、準備、解析三個階段,而驗證是確保類加載的正確性、準備是爲類的靜態變量分配內存,並初始化爲默認值、解析是把 類中的符號引用轉換爲直接引用。而外面的初始化爲類的靜態變量賦值爲正確的值。而局部變量並無鏈接的階段,所以沒有賦值爲默認值這一階段,所以必須本身 初始化才能使用。

咱們在類的加載中提到類的靜態鏈接過程,可是還有一部分類是須要動態鏈接的,其中如下是須要動態鏈接的對象

一、  實例變量(類的變量或者局部變量)

二、  經過其餘榮報告期動態注入的變量(IOC)

三、  經過代碼注入的對象(void setObj(Object obj))

全部的動態鏈接都只有準備和解析階段,沒有再次校驗(校驗發生在鏈接前類的加載階段),其中局部變量不會再次引起準備階段。

前面咱們提到JVM的生命週期,在如下四種狀況下會引起JVM的生命週期結束

一、  執行了System.exit()方法

二、  程序正常運行結束

三、  程序在執行過程當中遇到了異常或者錯誤致使異常終止

四、  因爲操做系統出現錯誤而致使JVM進程終止

一樣,在如下狀況下會致使一個方法調用結束

一、  執行引擎遇到了方法返回的字節碼指令

二、  執行引擎在執行過程當中遇到了未在該方法內捕獲的異常

這時候不少人會有一個疑問:當程序返回以後它怎麼知道繼續在哪裏執行?這就用到了咱們JVM內存模型中提到了的PC計數器。方法退出至關於當前棧出棧,出棧後主要作了如下事情:

一、  回覆上層方法的局部變量表

二、  若是有返回值的話將返回值壓入到上層操做數棧

三、  調整PC計數器指向下一條指令

除了以上信息之外,棧幀中還有一些附加信息,如預留一部份內存用於實現一些特殊的功能,如調試信息,遠程監控等信息。

接下來咱們要說的是方法調用,方法調用並不等於方法執行,方法調用的任務是肯定調用方法的版本(調用哪個方法),在實際過程當中有可能發生在加載期間也有可能發生在運行期。Class的編譯過程並不包含相似C++的鏈接過程,只有在類的加載或者運行期纔將對應的符號引用修正爲真正的直接引用,大大的提高了Java的靈活性,可是也大大增長了Java的複雜性。

在類加載的第二階段鏈接的第三階段解析,這一部分是在編譯時就肯定下來的,屬於編譯期可知運行期不可變。在字節碼中主要包含如下兩種

一、  invokestatic 主要用於調用靜態方法,屬於綁定類的調用

二、  invokespecial 主要用於調用私有方法,外部不可訪問,綁定實例對象

還有一種是在運行時候解析的,只有在運行時才能肯定下來的,主要包含如下兩方面

一、  invokevirtual 調用虛方法,不肯定調用那一個實現類

二、  invokeinterface 調用接口方法,不肯定調用哪個實現類

咱們能夠經過javap的命令查看對應的字節碼文件方法調用的方式,以下圖所示

Java

方法在調用過程當中,把invokestaticinvokespecial定義爲非虛方法的調用,非虛方法的調用都是在編譯器已經肯定具體要調用哪個方法,在類的加載階段就完成了符號引用到直接引用的轉換。除了非虛方法之外,還有一種被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 }

這個例子的執行結果會是什麼呢?咱們來看一下結果

和你的預期一致麼?這個實際上是一個靜態分派的杯具,

manwoman兩個對象被轉型之後,經過特徵簽名匹配,只能匹配到對應的父類的重載方法,所以致使最終的結構都是執行父類的代碼。由於具體的類是在運行期才知道具體是什麼類型,而編譯器只肯定是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}的方式來查看對應的字節碼,而基於javaDalvikVM亦能夠經過platforms\android-1.6\tools目錄中的dx工具查看對應的字節碼。具體命令爲dx --dex –verbose --dump-to=packageName --dump-method=Demo.foo --verbose-dump Demo.class

基於棧的Hotspot的執行過程以下:

基於棧的

DalvikVM執行過程以下所示:

 而基於彙編語言的展現就是這樣的了

附:基於JVM的邏輯運算模型以下圖所示

所以執行到

JVM上的過程就是下面的形式

相關文章
相關標籤/搜索