JVM | Java程序如何執行

  • 類文件結構基礎

Class文件是一組以8位字節爲基礎的單位的二進制流,各個數據項目按照順序緊湊地排列在Class文件之中,中間沒有任何分隔符。
Class文件存儲結構中只有兩種數據類型:無符號數和表(表又是由多個無符號數或者其餘表構成)。無符號數屬於基本的數據類型,以u一、u二、u四、u8來分別表明1個字節、2個字節、4個字節、8個字節的無符號數。無符號數是Class類文件的基石。java

  • 字節碼指令基礎

參考:http://www.javashuo.com/article/p-noiftjmn-cz.htmljvm

  • 回顧JVM運行時數據區域

JVM | Java程序如何執行

  • 方法區:

線程共享,存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼。ide

  • 堆:

線程共享,存儲對象實例。性能

  • 程序計數器:

線程私有,存儲當前線程正在執行的虛擬機字節碼指令的偏移地址(指令行號)。優化

  • 虛擬機棧:

線程私有,描述Java方法執行的內存模型。spa

  • 方法執行的內存模型

JVM | Java程序如何執行

  • 局部變量表:

變量存儲空間,存放方法參數、方法內定義的局部變量。線程

  • 操做數棧:

執行操做的空間,執行過程當中,會有各類字節碼指令網操做數棧寫入和提取內容,入棧、出棧操做。3d

  • 動態鏈接:

每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用。
在每一次運行期間轉化爲直接引用的符號引用。
相對的在類加載階段或者第一次使用的時候就轉化爲直接引用的符號引用,轉化過程稱爲靜態解析。code

  • 返回地址:

方法被調用的位置。對象

  • 局部變量表slot槽複用問題:

    public class SlotReuse {
            public static void methodA(){
                    byte[] placeholder = new byte[64 * 1024 * 1024];
                    System.gc();
            }
            public static void methodB(){
                    {
                            byte[] placeholder = new byte[64 * 1024 * 1024];
                    }
                    System.gc();
            }
            public static void methodC(){
                    {
                            byte[] placeholder = new byte[64 * 1024 * 1024];
                    }
                    int a = 0;
                    System.gc();
            }
            public static void main(String[] args) {
    //        methodA();
    //        methodB();
    //        methodC();
            }
    }

虛擬機運行參數加上「-verbose:gc」,查看垃圾收集過程。

  • 執行methodA();

    [GC 69499K->66048K(251392K), 0.0010970 secs]
    [Full GC 66048K->65866K(251392K), 0.0098170 secs]

處於變量placeholder的做用域,不敢回收placeholder的內存。

  • 執行methodB();

    [GC 69499K->66048K(251392K), 0.0011820 secs]
    [Full GC 66048K->65866K(251392K), 0.0112650 secs]

邏輯上出了placeholder的做用域,placeholder的內存仍然沒有被回收,緣由就是離開了placeholder的做用域後,沒有任何對局部變量表的讀寫操做,placeholder本來所佔用的Slot槽尚未被其餘變量複用,做爲GC Roots一部分的局部變量表仍然保持着對它的關聯。

  • 執行methodC();

    [GC 69499K->66000K(251392K), 0.0012780 secs]
    [Full GC 66000K->330K(251392K), 0.0099390 secs]

placeholder本來所佔用的Slot槽被其餘變量複用,切斷了GC Roots關聯,正常垃圾回收。

  • 如何找到正確的方法

肯定被調用方法的版本(即調用哪個方法)是方法調用階段的惟一任務。

  • 5種方法調用字節碼指令:

1)invokestatic:調用靜態方法。
2)invokespecial:調用實例構造器<init>方法、私有方法和父類方法。
3)invokevirtual:調用全部的虛方法。非虛方法之外的都是虛方法,非虛方法包括使用invokestatic、invokespecial調用的方法和被final修飾的方法。
4)invokeinterface:調用接口方法,運行時再肯定一個實現此接口的對象。
5)invokedynamic:用於在運行時動態解析出調用點限定符所引用的方法,並執行該方法。
ireturn(返回值是boolean、byte、char、short、int)、lreturn、freturn、dreturn、areturn:方法返回指令。

  • 靜態解析

方法在程序真正運行以前就有一個可肯定的調用版本,而且這個調用版本在運行期不可變。
只要能被invokestatic和invokespecial指令調用的方法,均可以在解析階段中肯定惟一的調用版本,符合這個條件的有靜態方法、私有方法、實例構造器、父類方法4類。

public class StaticResolution {
            public static void sayHello(){
                    System.out.println("hello world");
            }

            public static void main(String[] args) {
                    sayHello();
            }
 }

字節碼:

public static void main(java.lang.String[]);
         Signature: ([Ljava/lang/String;)V
         flags: ACC_PUBLIC, ACC_STATIC
         Code:
             stack=0, locals=1, args_size=1
             0: invokestatic  #5                  // Method sayHello:()V
             3: return
         LineNumberTable:
             line 14: 0
             line 15: 3
  • 靜態分派

全部依賴靜態類型來定位方法執行版本的分派動做。
分派時機:編譯階段
典型應用:方法重載
e.g.

public class StaticDispatch {
        public static void sayHello(short arg){
                System.out.println("hello short");
        }
        public static void sayHello(byte arg){
                System.out.println("hello byte");
        }
        public static void sayHello(Object arg){
                System.out.println("hello Object");
        }
        public static void sayHello(int arg){
                System.out.println("hello int");
        }
        public static void sayHello(long arg){
                System.out.println("hello long");
        }
        public static void sayHello(char arg){
                System.out.println("hello char");
        }
        public static void sayHello(char... args){
                System.out.println("hello char...");
        }
        public static void sayHello(Serializable arg){
                System.out.println("hello Serializable");
        }
        public static void main(String[] args) {
                sayHello('a');
        }
}

當前main方法編譯字節碼:

public static void main(java.lang.String[]);
Signature: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
  stack=1, locals=1, args_size=1
     0: bipush        97
     2: invokestatic  #12                 // Method sayHello:(C)V
     5: return
  LineNumberTable:
    line 46: 0
    line 47: 5

註釋public static void sayHello(char arg)構造方法後編譯字節碼:

public static void main(java.lang.String[]);
Signature: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
  stack=1, locals=1, args_size=1
     0: bipush        97
     2: invokestatic  #11                 // Method sayHello:(I)V
     5: return
  LineNumberTable:
    line 46: 0
    line 47: 5

自動轉型順序:char --> int --> long --> float --> double --> Serializable --> Object --> char...(可變長字符)
能夠寬化,不能夠窄化。

  • 動態分派

運行期根據實際類型肯定方法執行版本的分派動做。
分派時機:運行階段
典型應用:方法重寫
e.g.

public class DynamicDispatch {
            static abstract class Human {
                    protected abstract void sayHello();
            }
            static class Man extends Human {
                    @Override
                    protected void sayHello() {
                            System.out.println("man say hello");
                    }
            }
            static class Woman extends Human {
                    @Override
                    protected void sayHello() {
                            System.out.println("woman say hello");
                    }
            }
            public static void main(String[] args) {
                    Human man = new Man();
                    Human woman = new Woman();
                    man.sayHello();
                    woman.sayHello();
                    man = new Woman();
                    man.sayHello();
            }
    }

字節碼:

public static void main(java.lang.String[]);
        Signature: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
            stack=2, locals=3, args_size=1
                 0: new           #2                  // class edu/atlas/demo/java/jvm/DynamicDispatch$Man
                 3: dup
                 4: invokespecial #3                  // Method edu/atlas/demo/java/jvm/DynamicDispatch$Man."<init>":()V
                 7: astore_1
                 8: new           #4                  // class edu/atlas/demo/java/jvm/DynamicDispatch$Woman
                11: dup
                12: invokespecial #5                  // Method edu/atlas/demo/java/jvm/DynamicDispatch$Woman."<init>":()V
                15: astore_2
                16: aload_1
                17: invokevirtual #6                  // Method edu/atlas/demo/java/jvm/DynamicDispatch$Human.sayHello:()V
                20: aload_2
                21: invokevirtual #6                  // Method edu/atlas/demo/java/jvm/DynamicDispatch$Human.sayHello:()V
                24: new           #4                  // class edu/atlas/demo/java/jvm/DynamicDispatch$Woman
                27: dup
                28: invokespecial #5                  // Method edu/atlas/demo/java/jvm/DynamicDispatch$Woman."<init>":()V
                31: astore_1
                32: aload_1
                33: invokevirtual #6                  // Method edu/atlas/demo/java/jvm/DynamicDispatch$Human.sayHello:()V
                36: return
            LineNumberTable:
                line 32: 0
                line 33: 8
                line 34: 16
                line 35: 20
                line 36: 24
                line 37: 32
                line 38: 36
  • invokevirtual指令的運行時解析過程:
    1)找到操做數棧頂的第一個元素所指向的對象的實際類型,記做C。
    2)若是在類型C中找到與常量中的描述符和簡單名稱都相等的方法,則進行訪問權限校驗,
    若是經過則返回這個方法的直接引用,查找過程結束;
    若是不經過,則返回java.lang.IllegalAccessError異常。
    3)不然,按照繼承關係從下往上依次對C的各個父類進行第2步的搜索和驗證過程。
    4)若是始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。

因爲invokevirtual指令的第一步就是在運行期間肯定接收者的世界類型,因此兩次調用的invokevirtual指令把常量池中類方法符號引用解析到了不一樣的直接引用上,這個過程就是Java語言方法重寫的本質。

  • 虛擬機動態分派的實現

上面介紹了動態分派的過程,然而因爲動態分派是很是頻繁的動做,所以虛擬機實際實現中基於性能的考慮,大部分實現都不會真正地進行如此頻繁的搜索。經常使用的穩定優化手段就是爲類在方法區創建一個虛方法表(vtable),接口方法創建接口方法表(itable)。
虛方法表存放着各個方法的實際入口地址。
若是某個方法在子類中沒有被重寫,那麼子類的虛方法裏面的地址和父類相同方法的地址入口是一致的,都指向父類方的實現入口。
若是子類中重寫了這個方法,子類方法表中的地址將會替換爲指向子類實現版本的入口地址。
方法表通常在類加載的鏈接階段進行初始化,準備了類的變量初始值後,虛擬機會把該類的方法表也初始化完畢。

  • 如何執行方法內的字節碼

  • 基於棧的解釋器執行過程
    e.g.
public static int methodA(int a, int b){
    return a + b;
}
public static int methodB(int a, int b){
    return a - b;
}
public static int methodC(int a, int b){
    return a * b;
}
public static int heavyMethod(){
    int a = 200;
    int b = 100;
    int c = methodC(methodA(a, b), methodB(a, b));
    return c;
}

字節碼:

public static int methodA(int, int);
        Signature: (II)I
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
            stack=2, locals=2, args_size=2
                 0: iload_0
                 1: iload_1
                 2: iadd
                 3: ireturn
            LineNumberTable:
                line 18: 0

    public static int methodB(int, int);
        Signature: (II)I
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
            stack=2, locals=2, args_size=2
                 0: iload_0
                 1: iload_1
                 2: isub
                 3: ireturn
            LineNumberTable:
                line 22: 0

    public static int methodC(int, int);
        Signature: (II)I
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
            stack=2, locals=2, args_size=2
                 0: iload_0
                 1: iload_1
                 2: imul
                 3: ireturn
            LineNumberTable:
                line 26: 0

    public static int heavyMethod();
        Signature: ()I
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
            stack=3, locals=3, args_size=0
                 0: sipush        200
                 3: istore_0
                 4: bipush        100
                 6: istore_1
                 7: iload_0
                 8: iload_1
                 9: invokestatic  #17                 // Method methodA:(II)I
                12: iload_0
                13: iload_1
                14: invokestatic  #18                 // Method methodB:(II)I
                17: invokestatic  #19                 // Method methodC:(II)I
                20: istore_2
                21: iload_2
                22: ireturn
            LineNumberTable:
                line 128: 0
                line 129: 4
                line 130: 7
                line 140: 21

圖例分析:

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

JVM | Java程序如何執行

相關文章
相關標籤/搜索