我們知道,java文件經過編譯後轉換爲class文件,然後經過類加載子系統加載到jvm中執行,這個過程如下圖所示:
class文件結構
編譯過程就是把java文件變爲class文件的過程,用javac命令就可以,比如下面一段簡單的代碼:
public class Math {
public static final int initData = 666;
public int cal(){
int a = 1;
int b = 2;
int c = (a+b)*10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.cal();
}
}
經過javac編譯後得到的class文件如下:
我們還可以進一步用javap反編譯輸出到一個txt文件
javap -p -v Math.class >Math.txt
Classfile /E:/project/jvm-case/src/main/java/com/example/jvmcase/basic/Math.class
Last modified 2020-5-31; size 442 bytes
MD5 checksum 1f3ccea6e6faf14ff0e1c70b28bf920e
Compiled from "Math.java"
public class com.example.jvmcase.basic.Math
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#20 // java/lang/Object."<init>":()V
#2 = Class #21 // com/example/jvmcase/basic/Math
#3 = Methodref #2.#20 // com/example/jvmcase/basic/Math."<init>":()V
#4 = Methodref #2.#22 // com/example/jvmcase/basic/Math.cal:()I
#5 = Class #23 // java/lang/Object
#6 = Utf8 initData
#7 = Utf8 I
#8 = Utf8 ConstantValue
#9 = Integer 666
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 cal
#15 = Utf8 ()I
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 SourceFile
#19 = Utf8 Math.java
#20 = NameAndType #10:#11 // "<init>":()V
#21 = Utf8 com/example/jvmcase/basic/Math
#22 = NameAndType #14:#15 // cal:()I
#23 = Utf8 java/lang/Object
{
public static final int initData;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 666
public com.example.jvmcase.basic.Math();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public int cal();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 6: 0
line 7: 2
line 8: 4
line 9: 11
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/example/jvmcase/basic/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method cal:()I
12: pop
13: return
LineNumberTable:
line 13: 0
line 14: 8
line 15: 13
}
SourceFile: "Math.java"
這個文件看起來更具有可讀性,反編譯後看到的內容跟class文件是一一對應的,比如說魔數,版本號信息,常量池,字段表集合,方法表集合等信息
類加載
class文件最終要運行必須要通過類加載器加載到jvm中才能正常執行,類的整個生命週期如下圖,而類加載的全過程是前面三步,即裝載,鏈接,初始化
裝載
(1)通過類加載器classLoader獲取一個類的全限定名來獲取描述此類的二進制流,
(2)將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構
(3)在Java堆中生成一個代表這個類的java.lang.Class對象,作爲方法區中這些數據的訪問入口
類加載器
裝載的整個過程是由類加載器來完成的,當類加載器接收到類加載的請求時,它並不會馬上去加載該類,而是先自低向上先檢查該類是否已經加載,如果已經加載,則不會重新再加載。
如果該類沒有被加載,則優先由最頂層父類加載,頂層父類沒法加載的時候再給到下一個子類。
也就是說檢查階段是由低向高,加載順序是由高向低,這就是雙親委派模型
驗證
其實就是驗證class文件格式的正確性,驗證的內容包括魔數,版本號,常量池,方法等,其實就是對整個class文件的內容格式是否能夠給當前虛擬機正常執行的一個驗證。
準備
準備階段是正式爲類變量(static成員變量)分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量
public static final int initData = 666;
這個時候initData 的值爲0
解析
解析階段虛擬機是把常量池內的符號引用替換成直接引用的過程。就是對方法、字段、類信息的一個描述落地到真正在內存中開闢一塊區域。
運行時數據區
方法區跟堆是線程共享的,在虛擬機啓動時創建,上面其實已經分析過了,方法區主要存的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據,堆存的是對象。
堆的內存佈局如下:
整個堆分成新生代跟老年代兩部分(新生代佔1/3,老年代佔2/3),新生代又分爲eden區跟survivor區,每次創建的對象都是放在eden區,當eden區滿的時候觸發一次minor GC,然後把存活的對象移動到s0區,下次minor GC會把eden區跟s0區存活的對象移動到s1區,如此往復,其實就是每次minor GC的時候都會清空eden區跟survivor區的一半,然後把存活的對象移動到survivor區的另一半,並且對象每移動一次年齡就會增加1(對象頭mark word有個分代年齡標記位),當年齡增加到15的時候,對象就會進入老年代。
假如這個時候創建了一個很大的對象,新生代沒有足夠的空間去放,那麼這個對象會直接進入到老年代。
虛擬機棧
虛擬機棧是描述java方法執行的內存區域,隨着線程創建而創建,每個方法在執行的時候都會創建一個棧幀,用於存貯局部變量表,操作數棧,動態鏈接,方法出口燈信息,比如說我們開頭的例子,當執行到main方法的時候會創建一個main棧幀,main裏面調用cal方法也同時創建一個cal棧幀
結合代碼理解局部變量表和操作數棧
操作數棧也就是程序在運行過程中要做運算的數臨時存放的一塊內存區域,操作結束後數據彈出棧,同時把操作數棧清空,比如
iconst_1 將int類型常量1壓入操作數棧
istore_1 將int類型值存入局部變量1
也就是將1賦給a的過程是在操作數棧中完成的,完成之後把操作數棧清空,然後把a=1放在局部變量表
public int cal(){
int a = 1;
int b = 2;
int c = (a+b)*10;
return c;
}
反編譯後:
public int cal();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 6: 0
line 7: 2
line 8: 4
line 9: 11
動態鏈接
比如說
Math math = new Math();
math.cal();
在math調用cal()方法的時候,通過動態鏈接可以找到cal()方法代碼存放的位置,也就是說動態鏈接存放的是方法的內存位置
方法出口
當一個方法開始執行後,只有兩種方式可以退出,一種是遇到方法返回的字節碼指令,也就是ireturn,這個時候方法出口存放的信息就是回到main方法對應執行的cal()方法的地方,說白了就是返回上一個棧幀執行的地方;另一種是遇見異常,並且這個異常沒有在方法體內得到處理。
本地方法棧
如果當前線程執行的方法是Native類型的,這些方法就會在本地方法棧中執行
程序計數器
由於java是多線程執行的,所以每個棧都會有一個私有的程序計數器,用來記錄當前線程執行到的位置,當下次再搶到資源的時候就從程序計數器拿到的地址繼續往下執行。
如果線程正在執行Java方法,則計數器記錄的是正在執行的虛擬機字節碼指令的地址;
如果正在執行的是Native方法,則這個計數器爲空。