JVM 內部原理(五)— 基本概念之 Java 虛擬機器官方規範文件,第 7 版
介紹
版本:Java SE 7
每位使用 Java 的程式設計師都知道 Java 位元組碼在 Java 執行時(JRE - Java Runtime Environment)裡執行。Java 虛擬機器(JVM - Java Virtual Machine)是 Java 執行時(JRE)的重要組成部分,它可以分析和執行 Java 位元組碼。Java 程式設計師不需要知道 JVM 是如何工作的。有很多應用程式和應用程式庫都已開發完成,但是它們並不需要開發者對 JVM 有深入的理解。但是,如果你理解 JVM ,那麼就可以對 Java 更有了解,這也使得那些看似簡單而又難以解決的問題得以解決。
在本篇文章中,我會解釋 JVM 是如何工作的,它的結構如何,位元組碼是如何執行的及其執行順序,與一些常見的錯誤及其解決方案,還有 Java 7 的新特性。
目錄
虛擬機器(Virtual Machine)
Java 位元組碼
症狀
原因
類檔案格式(Class File Format)
症狀
原因
JVM 結構
類裝載器(Class Loader)
執行時資料區
執行引擎
Java 虛擬機器官方規範文件,第 7 版
- 分支語句中的字串
總結
內容
Java 虛擬機器官方規範文件,第 7 版
在 2011 年 7 月 28 日,Oracle 釋出了 Java SE 7 並更新了 JVM 官方規範文件至 Java SE 7 的版本。在 1999 年釋出《Java 虛擬機器官方規範文件,第二版》後,Oracle 花了 12 年時間做這版更新。更新版本的內容包括這 12 年來積累的各種變更修改,規範文件的描述更為清晰。除此之外,它還反映了《Java 語言規範文件,第七版》的內容。主要的更新概括如下:
- Java SE 5.0 引入泛型,支援方法的引數變數。
- 位元組碼驗證過程的技術從 Java SE 6 開始發生變化。
- 增加 invokedynamic 指令以及相關的類檔案格式支援動態型別語言。
- 刪除了對於 Java 語言本身的概念性描述,並將其歸入《Java 語言規範文件》中。
- 刪除了關於 Java 執行緒和鎖的描述,並將其寫入《Java 語言規範文件》。
最大的改變要數增加 invokedynamic 指令。這也意味著 JVM 內部指令集發生了變化,也就是說 JVM 從 Java SE 7 開始支援型別非固定的動態型別語言,如指令碼語言,以及動態的 Java 語言。之前沒有使用的操作碼(OpCode)186 被應用到新指令 invokedynamic 以及新的類檔案格式中以支援動態性,
由 Java 編譯器 Java SE 7 建立的類檔案版本是 51.0 。Java SE 6 的版本是 50.0 。類檔案格式發生了很大變化,因此 51.0 版本的類檔案不能執行於 Java SE 6 的 JVM 。
儘管有如此之多的變化,Java 方法的 65535 位元組長度限制並沒有被移除。除非 JVM 類檔案格式發生了創新式的變化,否則它也不太可能在將來移除。
Oracle Java SE 7 VM 支援 G1 ,這個新的垃圾回收機制;不過,它僅限於 Oracle JVM ,所以 JVM 本身並不受限於任何垃圾回收機制。因此,JVM 官方規範文件並沒有對此進行描述。
分支語句中的字串
Java SE 7 增加了多種語法和特性。不過,與 Java SE 7 中語言發生的許多變化相比,JVM 並沒有發生很多變化。那麼,Java SE 7 的新特性是如何實現的呢?我們通過反編譯看看 String 在分支語句中(一個將字串傳入 switch() 語句進行比較的功能)的實現方式。
有如下程式碼:
// SwitchTest
public class SwitchTest {
public int doSwitch(String str) {
switch (str) {
case "abc": return 1;
case "123": return 2;
default: return 0;
}
}
}
因為它是 Java SE 7 的新功能,所以它不能使用 Java SE 6 或更低版本的編譯器來編譯。用 Java SE 7 的 javac 編譯它。用 javap -c 顯示編譯結果:
C:Test>javap -c SwitchTest.classCompiled from "SwitchTest.java"
public class SwitchTest {
public SwitchTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return public int doSwitch(java.lang.String);
Code:
0: aload_1
1: astore_2
2: iconst_m1
3: istore_3
4: aload_2
5: invokevirtual #2 // Method java/lang/String.hashCode:()I
8: lookupswitch { // 2
48690: 50
96354: 36
default: 61
}
36: aload_2
37: ldc #3 // String abc
39: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
42: ifeq 61
45: iconst_0
46: istore_3
47: goto 61
50: aload_2
51: ldc #5 // String 123
53: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
56: ifeq 61
59: iconst_1
60: istore_3
61: iload_3
62: lookupswitch { // 2
0: 88
1: 90
default: 92
}
88: iconst_1
89: ireturn
90: iconst_2
91: ireturn
92: iconst_0
93: ireturn
位元組碼的內容要比 Java 原始碼多很多。首先,可以看到 lookupswitch 指令已經被 switch() 語句使用到位元組碼中。但是,有兩個 lookupswitch 指令而不是一個。反編譯後可以看到 int 傳入 switch() 語句,但是隻有一個 lookupswitch 指令用到了。也就是說 switch() 語句被拆分成了兩個語句來處理字串。檢視 #5、#39 和 #53 號位元組指令看 switch() 語句是如何處理字串的。
在 #5 和 #8 號位元組中,首先,hashCode() 方法被執行,然後 switch(int) 是通過使用 hashCode() 方法的執行結果來執行的。在 lookupswitch 指令的括號中,分支根據 hashCode 的值來定位到不同地方。字串 “abc” 的 hashCode 結果是 96354 ,被定位到 #36 號位元組。字串 “123” 的 hashCode 結果是 48690 ,被定位到 #50 號位元組。
在 #36、#37、#39 和 #42 號位元組中,可以發現接收的 str 變數值作為引數與 String “abc” 和 equals() 方法比較。如果結果相同,“0” 被插入到 #3 號本地變數列表的索引位置,字串被移動到 #61 號位元組。
通過這種方式,在 #50、#51、#53 和 #56 位元組中,可以發現接收的 str 變數值作為引數與 String “123” 和 equals() 方法比較。如果結果相同,“1” 被插入到 #3 號本地變數列表的索引位置,字串被移動到 #61 號位元組。
在 #61 和 #62 號位元組中,以 #3 號本地變數列表的索引位置的值,如:“0”、“1” 或其他值,進行 lookupswitch 和分支處理。
換句話說,在 Java 程式碼中,接收的 str 變數值作為 switch() 引數使用 hashCode() 方法和 equals() 方法進行比較。switch() 方法根據 int 值的結果執行。
在這個結果中,被編譯的位元組碼與之前的 JVM 規範文件並沒有任何不同。Java SE 7 的新特性,字串分支語句,是由 Java 編譯器來處理的,而不是 JVM 本身。以類似的方式,Java SE 7 的其他新特性也是通過 Java 編譯器進行處理的。
總結
這裡評審 Java 語言是如何設計讓 Java 可以更容易的使用沒有太大必要。有很多 Java 程式設計師並沒有對 JVM 有很深入的瞭解,卻也開發出了很多優秀的應用和庫。不過,如果能夠深入理解 JVM ,我們就能處理這些例子中出現的問題。
除了這裡提到的內容,JVM 有很多特性和技術。JVM 規範文件為 JVM 廠商提供了靈活的空間,讓他們應用各種不同的技術從而提供更好的效能。特別是垃圾回收技術,它被大多數語言使用,提供與 VM 類似的使用方式以及最新最前沿的效能優化技術。但是,這些內容在很多卓越的研究中都會被討論到,在此不作深入解釋。
參考
參考來源:
JVM Specification SE 7 - Run-Time Data Areas
2011.01 Java Bytecode Fundamentals
2012.02 Understanding JVM Internals
2013.04 JVM Run-Time Data Areas
Chapter 5 of Inside the Java Virtual Machine
2012.10 Understanding JVM Internals, from Basic Structure to Java SE 7 Features