本來JVM的工作原理淺到可以泛泛而談,但如果真的想把JVM工作機制弄清楚,實在是很難,涉及到的知識領域太多。所以,本文通過簡單的mian方法執行,淺談JVM工作原理,看看JVM里面都發生了什么。
先上代碼:
public class Test { private int invar = 1; public static String concat(String str1, String str2) { return str1+str2; } public int f() { return invar; } public static void main(String[] args) { Test test = new Test(); test.f(); int localInt1 = 1; int localInt2 = 2; int sum = localInt1 + localInt2; String str = concat("string ", "concat"); System.out.println(str); } }
再看看JVM內部結構:
上圖是對《The Java® Virtual Machine Specification Java SE 7 Edition》中JVM內部結構的一個描述簡圖,“The Java® Virtual Machine Specification”是JVM的一個抽象規范,主流JVM實現的主要有Oracle HotSpot、IBM J9等。
現在來看看啟動org.test.Test的main方法,JVM(本文涉及到的JVM implementation是針對JDK 7的HotSpot,下同)會做些什么:
1. JVM啟動:
根據JVM的啟動參數分配JVM runtime data area內存空間,如根據-Xms、-Xmx分配Heap大小;根據-XX:PermSize、-XX:MaxPermSize分配Method area大小;根據-Xss分配JVM Stack大小。注意,Method area、Heap是所有JVM線程都共享的,在JVM啟動時就會創建且分配內存空間;JVM Stack、PC Register、Native Method Stack是每個線程私有的,都是在線程創建時才分配。在HotSpot中,沒有JVM Stacks和Native Method Stacks之分,功能上已經合並。官方說明:Both Java programming language methods and native methods share the same stack.
2. Loading, Linking, and Initializing
所有這些工作都是Class Loader Subsystem來完成,對org.test.Test.class進行加載、鏈接、初始化。
2.1 Loading
將Test.class加載為Method area的Test Class data,Test.class是一個二進制文件,里面是JVM編譯器對org.test.Test.java編譯成的字節碼,關於.class字節碼的解讀,請看《實例分析Java Class的文件結構》,講得非常透徹。Test.class在Method area是如何存儲的?這個問題的解答,首先還是要對Method area有一個認識。先看看《The Java® Virtual Machine Specification Java SE 7 Edition》中對ClassFile的定義:
也就是說Test.class里面的二進制碼是按照ClassFile的結構一個一個字節來存儲相應的Test.java編譯后的信息。所有這些信息被類加載器加載會對應地存儲到Method area中,盡管體現的方式不一樣,例如:Test.class中的Constant pool對應Method area中的Runtime constant pool,Runtime constant pool中的Constant中的信息都是從Constant pool中獲取到的,Runtime constant pool里面都是些符號引用、字符串字面值以及整形、浮點型常量。下面是“JVM Method Area Class Data結構示意圖”:
實際上,在JVM的method area中,一個class或interface是由其全限定名稱和真正加載該類或接口的類加載器(即the defining classloader)來唯一確定的,因為一個類(如果無特殊說明,“類”表示class或interface,下同)可以由不同的類加載器加載。
這里要弄清楚《The Java® Virtual Machine Specification Java SE 7 Edition》中關於類加載器的兩個概念:the defining loader和the initiating loader。假設有兩個類加載器classloader1和classloader2,classloader1是classloader2的parent classloader(委派的),一個待加載的類A,現在classloader2加載類A,首先委派classloader1去加載類A,結果classloader1加載類A成功了,那這里classloader1就是類A的“the defining loader”即,classloader2就是類A的the initiating loader即
,因為類加載動作由classloader2發起的。關於類的可見性:對於類A、類B,類A是否對類B可見(即類B能找到類A)取決於類B的“the defining loader”能否自己或通過委派加載類A成功。
所以在method area中,首先根據“the defining loader”來划分所加載的類的范圍,這是邏輯意義上的概念,而對於每一個Class Data里面存儲什么內容,上圖已經比較清晰給出答案。這里需要注意的是Class的static變量是存儲在Field data中,而final static則存儲在Run-time constant pool中,Code的JVM指令存儲在Method data中,另外,Class Data中還有兩個引用:一個是指向“the defining loader”的引用,該引用在Class的動態鏈接解析時需要用到,如果該Class中引用了其他類,那么在動態鏈接解析時,就用該類的“the defining loader”去找其他類,完成其加載、鏈接、初始化動作;另一個是指向Class實例(即java.lang.Class對象)的引用,而JDK提供java.lang.Class出來,這樣java開發者就可以通過該引用獲取到存儲在method area中的Class Data,即有哪些Field、哪些Method,這就是大家熟悉的反射。
JVM在加載Test.class到Method area后,在使用其之前,還需要做一些工作,就是接下來的Linking和Initializing。
2.2 Linking
鏈接一個類包括對該類、其直接超類、其直接superinterfaces進行驗證(verifying)、准備(preparing),如果該類為數組類型,還包括對數組元素類型的鏈接。Loading, Linking, and Initializing的時間順序遵循以下兩個原則:
(1)一個類一定要在鏈接之前加載完成;
(2)一個類的驗證和准備一定要在初始化之前完成。
關於類中的符號引用什么時候進行解析,這個要看JVM的實現。例如有的JVM實現采用懶解析("lazy"or "late" resolution),即在需要進行該符號引用的解析時才去解析它,這樣的話,可能該類都已經初始化完成了,如果其他的類鏈接到該類中的符號引用,需要進行解析,這個時候才會去解析;還有的JVM實現采用靜態解析("eager" or "static" resolution),即在該類在進行驗證時一次性將其中的符號引用全部解析完成。在JVM的實現中,一般采用懶解析。
2.2.1 verifying
驗證.class文件在結構上滿足JVM規范的要求,驗證工作可能會引起其他類的加載但不進行驗證和准備。
2.2.2 preparing
給Class static變量分配內存空間,初始化為默認值即將內存空間清零。
關於對類的方法聲明,需要滿足以下加載約束條件:
假設為類C的“the defining loader”即
,
為類D的“the defining loader”即
,D為C的超類或superinterface,對於C中所Override D的方法m,m的返回類型為
,參數類型為
,如果
不是數組類型,假設
為
,否則
為
的元素類型,對於i=1,...,n,如果
不是數組類型,假設
為
,否則
為
的元素類型,則對於i=0,...,n滿足:
,即
、
都能自己或通過委派成功加載
。
更進一步,假設為C的superinterface,
為C的superclass,
中聲明了方法m,
中聲明且實現了方法m,則對於i=0,...,n滿足:
。
2.2.3 resolution
詳細見:Resolution in《The Java® Virtual Machine Specification Java SE 7 Edition》
2.3 Initializing
詳細見:Initialization in《The Java® Virtual Machine Specification Java SE 7 Edition》
3. 啟動main線程
在JVM實現中,線程為Execution Engine的一個實例,main函數是JVM指令執行的起點,JVM會創建main線程來執行main函數,以觸發JVM一系列指令的執行,真正地把JVM run起來。在創建main線程時,會為其分配私有的PC Register、JVM Stack、Native Method Stack,當然在HotSpot的實現中,JVM Stack、Native Method Stack功能上已經合並,下面以HotSpot為例來說說main函數的執行。
4. 執行main函數
先用javap -c Test.class,通過反匯編把Test.class對應的JVM機器碼弄出來:
Compiled from "Test.java" public class org.test.Test { public org.test.Test(); Code: 0: aload_0 1: invokespecial #10 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #12 // Field invar:I 9: return public static java.lang.String concat(java.lang.String, java.lang.String); Code: 0: new #20 // class java/lang/StringBuilder 3: dup 4: aload_0 5: invokestatic #22 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String; 8: invokespecial #28 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 11: aload_1 12: invokevirtual #31 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 15: invokevirtual #35 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 18: areturn public int f(); Code: 0: aload_0 1: getfield #12 // Field invar:I 4: ireturn public static void main(java.lang.String[]); Code: 0: new #1 // class org/test/Test 3: dup 4: invokespecial #46 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #47 // Method f:()I 12: pop 13: iconst_1 14: istore_2 15: iconst_2 16: istore_3 17: iload_2 18: iload_3 19: iadd 20: istore 4 22: ldc #49 // String string 24: ldc #51 // String concat 26: invokestatic #52 // Method concat:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 29: astore 5 31: getstatic #54 // Field java/lang/System.out:Ljava/io/PrintStream; 34: aload 5 36: invokevirtual #60 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 39: return }
JVM指令后面的#index表示ClassFile中常量池“數組”的索引,實際上線程中每一個函數的執行都對應一個幀在JVM Stack中的壓入彈出,幀包含局部變量數組(用來存儲函數參數、局部變量等)、操作數棧(用於配合JVM指令的執行,存儲來自局部變量數組和類屬性的值及中間結果,其中操作數棧中的值可以為直接引用)、一個指向當前方法所在類的runtime constant pool(用於符號引用解析):
main線程調用main函數時,先創建一個main幀,根據編譯時期就已經確定的局部變量數組和操作數棧的大小分配內存空間,將內存空間清零,將main幀壓入main線程 Stack中:
看看main中的指令:
0: new #1 // class org/test/Test 3: dup
0: new #1:#1表示Test.class中常量池“數組”的索引,該索引位置為CONSTANT_Class_info常量,表示Test class,這里的new指令表示new一個org/test/Test對象,且將其引用壓入操作數棧中;3: dup:在操作數棧中,復制棧頂的操作數,同時將其壓入棧頂。
在執行JVM指令的過程中,main線程的PC register會記錄當前所執行的JVM指令的地址。執行完這兩條指令后,只有操作數棧有變化:

4:invokespecial #46 // 調用Method "<init>":()V
這條指令操作是:彈出操作數棧頂的元素,在該元素所指向的Test對象上,調用<init>方法,其實就是對該對象進行初始化,<init>的參數為空,返回為V的類型即為空,執行完這條指令后,main幀如下:
7: astore_1 // 彈出操作數棧頂元素,將該引用存放到局部變量“數組”索引1的位置 8: aload_1 // 將局部變量“數組”索引1的位置的引用壓入操作數棧
執行完后,main幀如下:

9: invokevirtual #47 // 在Test對象上,調用Method f:()I
在執行invokevirtual指令時,在main函數中又調用了函數f(),所以main線程會創建一個幀即f幀,為f幀分配相應的內存空間,保存main幀的狀態,即保存局部變量數組、操作數棧中的值,以便f()調用 完成后再恢復,,將f幀壓入main線程的Stack,將f幀切換成當前幀,執行f()函數的字節碼,main線程PC register記錄當前執行的指令地址:

f幀中局部變量“數組”索引0的位置為什么是reference 1?這是因為在invokevirtual指令執行過程中,首先會將main幀中操作數棧中的棧頂元素彈出,將其傳給f幀,f幀將其存放到局部變量”數組“中,下面是執行f()函數中的指令:
0: aload_0 // 將局部變量“數組”索引0位置的引用壓入操作數棧中

1: getfield #12 // 彈出操作數棧頂元素即Test對象引用,在該引用指向的對象上獲取屬性Field invar:I的值,再將該值壓入操作數棧中

4: ireturn // 彈出f幀中操作數棧頂元素即1,將其壓入main幀中的操作數棧
執行ireturn指令,實際上表明f()函數調用完成返回,main線程會釋放f幀及其內存空間,將main幀切換成當前幀,恢復main幀的狀態,main線程的PC register記錄main幀中當前執行指令的地址,繼續執行完main幀后面的指令。
5. JVM退出
釋放main線程所占用資源及內存空間,如PC register、JVM Stack等,釋放JVM所占用的內存空間,如Heap、Method area,JVM退出。
雖然只是一個簡單的main方式執行,但通過這個簡單的示例可以看到JVM完整的工作流程。