JVM | Java程序如何執行
類文件結構基礎
Class文件是一組以8位字節為基礎的單位的二進制流,各個數據項目按照順序緊湊地排列在Class文件之中,中間沒有任何分隔符。
Class文件存儲結構中只有兩種數據類型:無符號數和表(表又是由多個無符號數或者其他表構成)。無符號數屬於基本的數據類型,以u1、u2、u4、u8來分別代表1個字節、2個字節、4個字節、8個字節的無符號數。無符號數是Class類文件的基石。
-
字節碼指令基礎
參考:http://blog.51cto.com/damon188/2131035
-
回顧JVM運行時數據區域
- 方法區:
線程共享,存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼。
- 堆:
線程共享,存儲對象實例。
- 程序計數器:
線程私有,存儲當前線程正在執行的虛擬機字節碼指令的偏移地址(指令行號)。
- 虛擬機棧:
線程私有,描述Java方法執行的內存模型。
-
方法執行的內存模型
- 局部變量表:
變量存儲空間,存放方法參數、方法內定義的局部變量。
- 操作數棧:
執行操作的空間,執行過程中,會有各種字節碼指令網操作數棧寫入和提取內容,入棧、出棧操作。
- 動態連接:
每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用。
在每一次運行期間轉化為直接引用的符號引用。
相對的在類加載階段或者第一次使用的時候就轉化為直接引用的符號引用,轉化過程稱為靜態解析。
- 返回地址:
方法被調用的位置。
-
局部變量表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
}
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程序如何執行