系統性學習jvm請點擊jvm學習目錄
前言
在之前,我們講了java內存佈局,講了垃圾收集,將了字節碼文件,其實這些東西都很抽象,看了之後也很難和具體我們寫的代碼相聯繫起來,相信很多人其實都更關心自己的代碼是如何運行的。希望對java代碼的運行有一個直觀的瞭解。
那麼本篇博客就對java代碼的整個運行過程進行一個簡單的介紹,讓讀者對jvm能有一個更清晰的把握,不至於一上來就懵逼。
從java代碼到運行
下面我用一個例子來介紹java代碼從誕生到運行的過程吧。
首先我們開始編寫java代碼,爲了方便,編寫個比較簡單的吧。
public class Test {
public int calc() {
int a = 12;
int b = 34;
return a + b;
}
public static void main(String[] args) {
Test test = new Test();
test.calc();
}
}
如上所示一個簡單的java代碼。
在編寫完代碼之後,就是對代碼進行編譯,將java代碼Test類轉換成類Class文件,只有這個文件,jvm才能夠讀取。
這個步驟是編譯器完成的。
編譯器將.java文件編譯成類class文件。
其中類class文件的情況如下所示。
Classfile /H:/Test.class
Last modified 2020年5月31日; size 358 bytes
MD5 checksum 6758967fd1d4328a6721de106d960e46
Compiled from "Test.java"
public class Test
minor version: 0
major version: 53
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #2 // Test
super_class: #5 // java/lang/Object
interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #5.#16 // java/lang/Object."<init>":()V
#2 = Class #17 // Test
#3 = Methodref #2.#16 // Test."<init>":()V
#4 = Methodref #2.#18 // Test.calc:()I
#5 = Class #19 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 calc
#11 = Utf8 ()I
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 SourceFile
#15 = Utf8 Test.java
#16 = NameAndType #6:#7 // "<init>":()V
#17 = Utf8 Test
#18 = NameAndType #10:#11 // calc:()I
#19 = Utf8 java/lang/Object
{
public Test();
descriptor: ()V
flags: (0x0001) 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 2: 0
public int calc();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: bipush 12
2: istore_1
3: bipush 34
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: ireturn
LineNumberTable:
line 4: 0
line 5: 3
line 6: 6
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class Test
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method calc:()I
12: pop
13: return
LineNumberTable:
line 10: 0
line 11: 8
line 12: 13
}
SourceFile: "Test.java"
這裏如果對類class文件(字節碼文件)情況不清楚的可以點擊jvm學習 字節碼文件(Class類文件)結構介紹及快速理解
當有了類class文件之後,並做不了什麼,因爲此時Test類的信息還是在硬盤中存儲的。
當我們鍵入 java Test時,jvm便開始運行Test.class文件了。
此時需要將這個Test類加載到內存中去。該過程就是類加載過程。
jvm使用類加載器將Test類的信息加載到內存中去。(類加載之後我會專門寫)。
類加載器會將整個類class文件的核心常量池中的東西放到java內存中的運行時常量池中。將方法表集合關於方法的字節碼指令放到方法區中去。
下面,在運行時。從main作爲程序的進入點。
首先將main函數放進該線程的虛擬機棧,作爲第一個棧幀,然後根據方法表集合中的stack和locals來分配操作數棧和局部變量表。
上面類class文件中main()方法中code屬性下第一行就是:
那麼jvm就會給這個main函數棧幀分配一個深度爲2的操作數棧,然後其中的局部變量表大小爲2個變量槽。
當該分配完的東西分配完了之後,jvm將會執行棧頂的方法,也就是執行main方法。(main方法不詳細講,下面詳細的將calc方法)首先它創建一個Test類的實例對象test,然後調用test對象的calc()方法。
此時main方法中斷,保存當前運行現場。
calc()方法進入虛擬機棧,稱爲棧頂。
jvm此時開始運行calc()方法。
下面來結合字節碼指令詳細講一下calc()方法。
calc()方法的字節碼指令如下圖所示:
可以看到,該方法操作數棧深度爲2,局部變量表大小爲3個變量槽。
(字節碼指令是基於棧的指令,之前本科接觸過彙編指令,那其實是基於寄存器的指令,彙編指令可以將數據存儲在指定的寄存器中,每個指令基本都可以包含兩個輸入參數:寄存器和數值,例:mov eax 2。而字節碼指令基本沒有輸入,例:iconst 和2兩條指令是:將2壓入棧。所以字節碼指令的計算操作都是圍繞這操作數棧來進行。)
下面來介紹calc()方法具體執行過程。
-
首先開始執行前兩行指令,bipush 12是將12這個棧頂壓入操作數棧。注意,這裏可以看到是第0行指令之後便是第2行指令了。所以這裏bipush
12實際是2個指令,本身字節碼指令也是零地址指令,所以不要弄錯了。第0個指令是bipush,代表將之後一條指令存儲的數據放入操作數棧。第1個指令代表要存入操作數棧的數是12。 -
第2個指令istore_1以爲將操作數棧中的棧頂數據出棧,放入局部變量表的第一個變量槽存儲。所以12出棧,放入局部變量表槽1,操作數棧棧空。
-
同理,3,4指令就是34進棧。
-
同理,5指令是34存入局部變量表槽2。經過這些操作,操作數棧又空了。
-
第6個指令是iload_1是將局部變量表槽1內容12複製到操作數棧。
-
同理,第7個指令是將局部變量表槽2內容34複製到操作數棧。
-
第8個指令iadd意爲將操作數棧中的兩個棧頂元素出棧,並相加,並將結果再存入操作數棧。
-
第9個指令就很好理解了。返回棧頂元素。
之後calc()方法就執行完了。calc()方法棧幀出棧。
之後main方法又稱爲棧頂。在之前中斷的地方繼續往下執行。最終執行完畢。
整個java代碼的一生就可以認爲是結束了。
總結
總結一下,整個過程基本就是這樣的流程:java代碼–字節碼文件–加載進內存–方法進棧–執行–出棧。
在字節碼指令時,一定是圍繞着操作數棧來實現的,當然局部變量表的作用也很大。