[譯]深入理解JVM Understanding JVM Internals

笨拙的小菜鳥發表於2020-06-03

轉載:
英文原版地址:http://www.cubrid.org/blog/dev-platform/understanding-jvm-internals/
翻不了牆的可以看這個英文版:https://www.cnblogs.com/davidwang456/p/3464743.html
我找了個翻譯版看,但是圖片刷不出來:https://segmentfault.com/a/1190000004206269
國內英文版那個程式碼排版又很差,但是有圖,我這裡把兩個整合一下
http://itindex.net/detail/41088-理解-jvm-內幕

正文

每個使用Java的開發者都知道Java位元組碼是在JRE中執行(Java Runtime Environment Java執行時環境)。JRE中最重要的部分是 Java虛擬機器(JVM),JVM負責分析和執行Java位元組碼,Java開發人員並不需要去關心JVM是如何執行的。在沒有深入理解JVM的情況下,許多開發者已經開發出了非常多的優秀的應用以及Java類庫。不過,如果你瞭解JVM的話,你會更加了解Java的,並且你會輕鬆解決那些看似簡單但是無從下手的問題。

因此,在這篇檔案裡,我會闡述JVM是如何執行的,包括它的結構,它如何去執行位元組碼,以及按照怎樣的順序去執行,同時我還會給出一些常見錯誤的示例以及對應的解決辦法。最後,我還會講解Java 7中的一些新特性

虛擬機器

JRE是由Java API和JVM組成的。JVM的主要作用是通過Class Loader來載入Java程式,並且按照Java API來執行載入的程式。

虛擬機器(VM: Virtual Machine) 虛擬機器是通過軟體的方式來模擬實現的機器(比如說計算機),它可以像物理機一樣執行程式。設計虛擬機器的初衷是讓Java能夠通過它來實現 WORA( Write Once Run Anywhere 一次編譯,到處執行),儘管這個目標現在已經被大多數人忽略了。因此,JVM可以在不修改Java程式碼的情況下,在所有的硬體環境上執行 Java位元組碼。

JVM的基本特性:

  • 基於棧(Stack-based)的虛擬機器: 不同於Intel x86和ARM等比較流行的計算機處理器都是基於_暫存器(register)架構,JVM是_基於棧執行的
  • 符號引用(Symbolic reference): 除了基本型別以外的資料(類和介面)都是通過符號來引用,而不是通過顯式地使用記憶體地址來引用。
  • 垃圾回收機制: 類的例項都是通過使用者程式碼進行建立,並且自動被垃圾回收機制進行回收。
  • 通過明確清晰基本型別確保平臺無關性: 傳統的程式語言,例如C/C++,int型別的大小取決於不同的平臺。JVM通過對基本型別的清晰定義來保證它的相容性以及平臺獨立性。
  • 網路位元組序(Network byte order): Java class檔案的二進位制表示使用的是基於網路的位元組序(network byte order)。為了在使用小端(little endian)的Intel x86平臺和在使用了大端(big endian)的RISC系列平臺之間保持平臺無關,必須要定義一個固定的位元組序。JVM選擇了網路傳輸協議中使用的網路位元組序,即基於大端(big endian)的位元組序。

雖然是Sun公司開發了Java,但是所有的開發商都可以開發並且提供遵循Java虛擬機器規範的JVM。正是由於這個原因,使得Oracle HotSpot和IBM JVM等不同的JVM能夠並存。Google的Android系統裡的Dalvik VM也是一種JVM,雖然它並不遵循Java虛擬機器規範。和基於棧的Java虛擬機器不同,Dalvik VM是基於暫存器的架構,因此它的Java位元組碼也被轉化成基於暫存器的指令集。

Java 位元組碼(Java bytecode)

為了保證WORA,JVM使用Java位元組碼這種介於Java和機器語言之間的中間語言。位元組碼是部署Java程式碼的最小單位。

在解釋Java位元組碼之前,我們先通過例項來簡單瞭解它。這個案例是一個在開發環境出現的真實案例的總結。

現象

一個一直執行正常的應用突然無法執行了。在類庫被更新之後,返回下面的錯誤。

Exception in thread "main" java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V at com.nhn.service.UserService.add(UserService.java:14) at com.nhn.service.UserService.main(UserService.java:19)

程式程式碼如下,並在更新類庫之前未曾對這段程式碼做過變更:

// UserService.java … 
public void add(String userName) {
     admin.addUser(userName); 
}

類庫中更新過的程式碼前後對比如下:

// UserAdmin.java - 更新後的原始碼 …

public User addUser(String userName) {
     User user = new User(userName);
     User prevUser = userMap.put(userName, user);
     return prevUser;
 } 
// UserAdmin.java - 更新前的原始碼 … 
public void addUser(String userName) { 
    User user = new User(userName);
     userMap.put(userName, user); 
}

簡而言之,之前沒有返回值的addUser()被改修改成返回一個User類的例項的方法。不過,應用的程式碼沒有做任何修改,因為它沒有使用addUser()的返回值。

咋一看,com.nhn.user.UserAdmin.addUser()方法似乎仍然存在,如果存在的話, 那麼怎麼還會出現NoSuchMethodError的錯誤呢?

問題分析

上面問題的原因是在於應用的程式碼沒有用新的類庫來進行編譯。換句話來說,應用程式碼似乎是調了正確的方法,只是沒有使用它的返回值而已。不管怎樣,編譯後的class檔案表明了這個方法是有返回值的。你可以從下面的錯誤資訊裡看到答案。

可以通過下面的異常資訊說明這一點:
java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/langString;)V

NoSuchMethodError出現的原因是“com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V”方法找不到。注意一下”Ljava/lang/String;”和最後面的“V”。在Java位元組碼的表示式裡,”L;”表示的是類的例項。這裡表示addUser()方法有一個java/lang/String的物件作為引數。在這個類庫裡,引數沒有被改變,所以它是正常的。最後面的“V”表示這個方法的返回值。在Java位元組碼的表示式裡,”V”表示沒有返回值(Void)。綜上所述,上面的錯誤資訊是表示有一個java.lang.String型別的引數,並且沒有返回值的com.nhn.user.UserAdmin.addUser方法沒有找到。

因為程式程式碼是使用之前版本的類庫進編譯的,class檔案中定義的是應該呼叫返回"V"型別的方法(返回值為空)。然而,在改變類庫後,返回"V"型別(返回值為空)的方法已不存在,取而代之的是返回型別為"Lcom/nhn/user/User;"的方法。所以便發生了上面看到的NoSuchMethodError。

註釋
因為開發者未針對新類庫重新編譯程式程式碼,所以發生了錯誤。儘管如此,類庫提供者卻也要為此負責。因為之前沒有返回值的addUser()方法既然是public方法,但後面卻改成了會返回user實現,這意味著方法簽名發生了明顯的變化。這意味了該類庫不能對之前的版本進行相容,所以類庫提供者必須事前對此進行通知。

我們重新回到Java 位元組碼,Java 位元組碼 是JVM的基本元素,JVM本身就是一個用於執行Java位元組碼的執行器。Java編譯器並不會把像C/C++那樣把高階語言轉為機器語言(CPU執行指令),而是把開發者能理解的Java語言轉為JVM理解的Java位元組碼。因為Java位元組碼是平臺無關的,所以它可以在安裝了JVM(準確的說,是JRE環境)的任何硬體環境執行,即使它們的CPU和作業系統各不相同(所以在Windows PC機上開發和編譯的class檔案在不做任何調整的情況下就可以在Linux機器上執行)。編譯後檔案的大小與原始檔大小基本一致,所以比較容易通過網路傳輸和執行Java位元組碼。

The class file itself is a binary file that cannot be understood by a human. Java class-檔案是一種人很難去理解的二進位制檔案,所以我們很難直觀的理解其中的指令。為了便於理解它,JVM提供者提供了javap,反彙編器。使用javap產生的結果是Java組合語言。在上面的例子中,下面的Java彙編程式碼是通過javap -c對UserServiceadd()方法進行反彙編得到的。

public void add(java.lang.String);
  Code:
   0:   aload_0
   1:   getfield        #15; //Field admin:Lcom/nhn/user/UserAdmin;
   4:   aload_1
   5:   invokevirtual   #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)V
   8:   return

在這段Java彙編程式碼中,addUser()方法是在第四行的“5:invokevitual#23″進行呼叫的。這表示對應索引為23的方法會被呼叫。索引為23的方法的名稱已經被javap給註解在旁邊了。 invokevirtual是Java位元組碼裡呼叫方法的最基本的操作碼(Opcode)。在Java位元組碼裡,有四種操作碼可以用來呼叫一個方法,分別是:invokeinterface,invokespecial,invokestatic以及invokevirtual。操作碼的作用分別如下:

  • invokeinterface: 呼叫介面方法
  • invokespecial: 呼叫初始化方法、私有方法、或父類中定義的方法
  • invokestatic: 呼叫靜態方法
  • invokevirtual: 呼叫例項方法

Java 位元組碼的指令集包含操作碼(OpCode)和運算元(Operand)。像invokevirtual這樣的操作碼需要一個2位元組長度的運算元。(需要2個位元組的運算元。)

By compiling the application code above with the updated library and then disassembling it, the following result will be obtained.
對上面案例中的程式程式碼,如果在更新類庫後重新編譯程式程式碼,然後我們再反編譯位元組碼將看到如下結果:
用更新的類庫來編譯上面的應用程式碼,然後反編譯它,將會得到下面的結果:

public void add(java.lang.String);
  Code:
   0:   aload_0
   1:   getfield        #15; //Field admin:Lcom/nhn/user/UserAdmin;
   4:   aload_1
   5:   invokevirtual   #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;
   8:   pop
   9:   return

你會發現,對應索引為23的方法被替換成了一個返回值為”Lcom/nhn/user/User”的方法。

在上面的反編譯結果中,程式碼前面的數字是具有什麼含義?

表示該Opcode位於第幾個位元組(以0開始),大概這就是為什麼執行在JVM上面的程式碼成為Java“位元組”碼的原因。像 aload_0, getfield 和 invokevirtual 都被表示為一個單位元組數字。(aload_0 = 0x2a, getfiled = 0xb4, invokevirtual = 0xb6)。因此Java位元組碼錶示的最大指令碼為256。(一個位元組是2的8次方,可以表示2的8次方,0-255)

像aload_0和aload_1這樣的操作碼不需要任何運算元,因此aload_0的下一個位元組就是下一個指令的操作碼。而像getfield和invokevirtual這樣的操作碼卻需要一個2位元組的運算元,因此第一個位元組裡的第二個指令getfield指令的一下指令是在第4個位元組,其中跳過了2個位元組。通過16進位制編輯器檢視位元組碼如下:
0 1 2 3 4 5 6 7 8 9
2a b4 00 0f 2b b6 00 17 57 b1

(aload_0 = 0x2a, getfiled = 0xb4, invokevirtual = 0xb6) 怎樣計算來的??
aload_0, aload_1, getfield 和 invokevirtual 都為操作碼 佔一個位元組
getfield ,getfield 都需要兩個位元組的運算元。
0x2a --> aload_0 OpCode
0xb4 --> getfield OpCode
0x00 --> one of Operand of getfield OpCode
0x0f --> one of Operand of getfield OpCode
0x2b --> aload_1 OpCode
0xb6 --> invokevirtual Opcode
0x00 --> one of Operand of invokevirtual OpCode
0x17 --> one of Operand of invokevirtual OpCode
0x57 --> pop OpCode
0xb1 --> return OpCode
在Java位元組碼中,類例項表示為"L;",而void表示為"V",類似的其他型別也有各自的表示。下表列出了Java位元組碼中型別表示。

表1: Java位元組碼裡的型別表示

Java 位元組碼 型別 描述
B byte 單位元組
C char Unicode字元
D double 雙精度浮點數
F float 單精度浮點數
I int 整型
J long 長整型
L 引用 classname型別的例項
S short 短整型
Z boolean 布林型別
[ 引用 一維陣列

表2: Java程式碼的位元組碼示例

Java 位元組碼 Java 位元組碼錶示
double d[][][] [[[D
Object mymethod(int i, double d, Thread t) mymethod(I,D,Ljava/lang/Thread;)Ljava/lang/Object;

在《Java虛擬機器技術規範第二版》的4.3 描述符(Descriptors)章節中有關於此的詳細描述,在第6章"Java虛擬機器指令集"中介紹了更多不同的指令。

類檔案格式

現象

當我們編寫完jsp程式碼,並且在Tomcat執行時,Jsp程式碼沒有正常執行,而是出現了下面的錯誤。

Servlet.service() for servlet jsp threw exception org.apache.jasper.JasperException: Unable to compile class for JSP Generated servlet error: The code of method _jspService(HttpServletRequest, HttpServletResponse) is exceeding the 65535 bytes limit"

問題分析

在不同的Web伺服器上,上面的錯誤資訊可能會有點不同,不過有有一點肯定是相同的,它出現的原因是65535位元組的限制。這個65535位元組的限制是JVM規範裡的限制,它規定了 一個方法的大小不能超過65535位元組。

下面我會更加詳細地講解這個65535位元組限制的意義以及它出現的原因。

Java位元組碼裡的分支和跳轉指令分別是”goto"和"jsr"。

goto [branchbyte1] [branchbyte2] 
jsr [branchbyte1] [branchbyte2]

這兩個指令都接收一個2位元組的有符號的分支跳轉偏移量做為運算元,因此偏移量最大隻能達到65535。不過,為了支援更多的跳轉,Java位元組碼提供了"goto_w"和"jsr_w"這兩個可以接收4位元組分支偏移的指令。

goto_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4] 
jsr_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4]

受這兩個指令所賜,分支能表示的最大偏移遠遠超過了65535,這麼說來java 方法就不會再有65535個位元組的限制了。然而,由於Java 類檔案的各種其他限制,java方法的定義仍然不能夠超過65535個位元組的限制。下面我們通過對類檔案的解釋來看看java方法不能超過65535位元組的其他原因。

Java類檔案的大體結構如下:

ClassFile {
    u4 magic;
    u2 minor_version;
    u2 major_version;
    u2 constant_pool_count;
    cp_info constant_pool[constant_pool_count-1];
    u2 access_flags;
    u2 this_class;
    u2 super_class;
    u2 interfaces_count;
    u2 interfaces[interfaces_count];
    u2 fields_count;
    field_info fields[fields_count];
    u2 methods_count;
    method_info methods[methods_count];
    u2 attributes_count;
    attribute_info attributes[attributes_count];}

上面的檔案結構出自《Java虛擬機器技術規範第二版》的4.1節 類檔案結構

之前講過的UserService.class檔案的前16個位元組的16進製表示如下:
ca fe ba be 00 00 00 32 00 28 07 00 02 01 00 1b

我們通過對這一段符號的分析來了解一個類檔案的具體格式。

  • magic: 類檔案的前4個位元組是一組魔數,是一個用於區分Java類檔案的預定義值。如上所看到的,其值固定為0xCAFEBABE。也就是說一個檔案的前4個位元組如果是0xCAFABABE,就可以認為它是Java類檔案。"CAFABABE"是與"JAVA"有關的一個有趣的魔數。
  • minor_version, major_version: 接下來的4個位元組表示類的版本號。如上所示,0x00000032表示的類版本號為50.0。由JDK 1.6編譯而來的類檔案的版本號是50.0,而由JDK 1.5編譯而來的版本號則是49.0。JVM必須保持向後相容,即保持對比其版本低的版本的類檔案的相容。而如果在一個低版本的JVM中執行高版本的類檔案,則會出現java.lang.UnsupportedClassVersionError的發生。
  • constant_pool_count, constant_pool[]: 緊接著版本號的是類的常量池資訊。這裡的資訊在執行時會被分配到執行時常量池區域,後面會有對記憶體分配的介紹。在JVM載入類檔案時,類的常量池裡的資訊會被分配到執行時常量池,而執行時常量池又包含在方法區內。上面UserService.class檔案的constant_pool_count為0x0028,所以按照定義contant_pool陣列將有(40-1)即39個元素值。
  • access_flags: 2位元組的類的修飾符資訊,表示類是否為public, private, abstract或者interface。
  • this_class, super_class: 分別表示儲存在constant_pool陣列中的當前類及父類資訊的索引值。
  • interface_count, interfaces[]: interface_count為儲存在constant_pool陣列中的當前類實現的介面數的索引值,interfaces[]即表示當前類所實現的每個介面資訊。
  • fields_count, fields[]: 類的欄位數量及欄位資訊陣列。欄位資訊包含欄位名、型別、修飾符以及在constant_pool陣列中的索引值。
  • methods_count, methods[]: 類的方法數量及方法資訊陣列。方法資訊包括方法名、引數的型別及個數、返回值、修飾符、在constant_pool中的索引值、方法的可執行程式碼以及異常資訊。
  • attributes_count, attributes[]: attribute_info有多種不同的屬性,分別被field_info, method_into使用。

javap程式把class檔案格式以可閱讀的方式輸出來。在對UserService.class檔案使用"javap -verbose"命令分析時,輸出內容如下:

Compiled from "UserService.java"

public class com.nhn.service.UserService extends java.lang.Object
  SourceFile: "UserService.java"
  minor version: 0
  major version: 50
  Constant pool:const #1 = class        #2;     //  com/nhn/service/UserService
const #2 = Asciz        com/nhn/service/UserService;
const #3 = class        #4;     //  java/lang/Object
const #4 = Asciz        java/lang/Object;
const #5 = Asciz        admin;
const #6 = Asciz        Lcom/nhn/user/UserAdmin;;// … omitted - constant pool continued …

{
// … omitted - method information …

public void add(java.lang.String);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   aload_0
   1:   getfield        #15; //Field admin:Lcom/nhn/user/UserAdmin;
   4:   aload_1
   5:   invokevirtual   #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;
   8:   pop
   9:   return  LineNumberTable:
   line 14: 0
   line 15: 9  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   0      10      0    this       Lcom/nhn/service/UserService;
   0      10      1    userName       Ljava/lang/String; // … Omitted - Other method information …
}

由於篇幅原因,上面只抽取了部分輸出結果。在全部的輸出資訊中,會為你展示包括常量池和每個方法內容等各種資訊。

方法的65535個位元組的限制受到了method_info struct的影響。如上面"javap -verbose"的輸出所示,method_info結構包含Code,LineNumberTable,以及LocalViriable attribute幾個屬性,這個在“javap -verbose"的輸出裡可以看到。Code屬性裡的LineNumberTable,LocalVariableTable以及exception_table的長度都是用一個固定的2位元組來表示的。因此,方法的大小是不能超過LineNumberTable,LocalVariableTable以及exception_table的長度的,它們都是65535位元組(即不能超過65535個位元組)

儘管許多人都在抱怨方法的大小限制,JVM規範裡也聲稱了”這個長度以後有可能會是可擴充套件的“。不過,到現在為止,還沒有為這個限制做出任何動作。從JVM規範裡的把class檔案裡的內容直接拷貝到方法區這個特點來看,要想在保持後向相容性的同時來擴充套件方法區的大小是非常困難的。

對於一個由Java編譯器錯誤而導致的錯誤的類檔案將發生怎樣的情況?如果是在網路傳輸或檔案複製過程中,類檔案被損壞又將發生什麼?

為了應對這種場景,Java的類裝載器通過一個嚴格而且慎密的過程來校驗class檔案。在JVM規範裡詳細地講解了這方面的內容。

註釋 我們怎樣能夠判斷JVM正確地執行了class檔案校驗的所有過程呢?我們怎麼來判斷不同提供商的不同JVM實現是符合JVM規範的呢?
劃重點:驗證jvm
1.驗證jvm是否符合jvm規範
2.驗證jvm是否成功執行類檔案的驗證過程
為了能夠驗證以上兩點,Oracle提供了一個測試工具TCK(Technology Compatibility Kit)。這個TCK工具通過執行成千上萬的測試用例來驗證一個JVM是否符合規範,這些測試裡面包含了各種非法的class檔案。只有通過了TCK的測試的JVM才能稱作JVM。

和TCK相似,有一個組織JCP(Java Community Process; http://jcp.org)負責Java規範以及新的Java技術規範。對於JCP而言,如果要完成一項Java規範請求(Java Specification Request, JSR)的話,需要具備規範文件,可參考的實現以及通過TCK測試。任何人如果想使用一項申請JSR的新技術的話,他要麼使用RI提供許可的實現,要麼自己實現一個並且保證通過TCK的測試。

JVM 結構

Java程式的執行過程如下圖所示:
圖1: Java程式碼執行過程
圖1: Java程式碼執行過程

類裝載器負責裝載編譯後的位元組碼,並載入到執行時資料區(Runtime Data Area),然後執行引擎執行會執行這些位元組碼。

類載入

Java提供了動態的裝載特性;它會在執行時的第一次引用到一個class的時候對它進行裝載和連結,而不是在編譯期進行。JVM的類裝載器負責動態裝載。Java類裝載器有如下幾個特點:

  • 層次結構: Java的類載入器是按父子關係的層次結構組織的。Boostrap類載入器處於層次結構的頂層,是所有類載入器的父類。
  • 委派模式: 基於類載入器的層次組織結構,類載入器之間是可以進行委派的。當一個類需要被載入,會先去請求父載入器判斷該類是否已經被載入。如果父類加器已載入了該類,那它就可以直接使用而無需再次載入。如果尚未載入,才需要當前類載入器來載入此類。
  • 可見性限制: 子類載入器可以從父類載入器中獲取類,反之則不行。一個子裝載器可以查詢父裝載器中的類,但是一個父裝載器不能查詢子裝載器裡的類。
  • 不能解除安裝: 類裝載器可以裝載一個類但是不可以解除安裝它,不過可以刪除當前的類裝載器,然後建立一個新的類裝載器。

Each class loader has its namespace that stores the loaded classes. When a class loader loads a class, it searches the class based on FQCN (Fully Qualified Class Name) stored in the namespace to check whether or not the class has been already loaded. Even if the class has an identical FQCN but a different namespace, it is regarded as a different class. A different namespace means that the class has been loaded by another class loader.

每個類載入器都有自己的空間,用於儲存其載入的類資訊。當類載入器需要載入一個類時,它通過FQCN)(Fully Quanlified Class Name: 全限定類名)的方式先在自己的儲存空間中檢測此類是否已存在。在JVM中,即便具有相同FQCN的類,如果出現在了兩個不同的類載入器空間中,它們也會被認為是不同的。存在於不同的空間意味著類是由不同的載入器載入的。

下圖解釋了類載入器的委派模型:
類載入器的委派模型
圖2: 類載入器的委派模型

When a class loader is requested for class load, it checks whether or not the class exists in the class loader cache, the parent class loader, and itself, in the order listed. In short, it checks whether or not the class has been loaded in the class loader cache. If not, it checks the parent class loader. If the class is not found in the bootstrap class loader, the requested class loader searches for the class in the file system.

當JVM請示類載入器載入一個類時,載入器總是按照從類載入器快取、父類載入器以及自己載入器的順序查詢和載入類。也就是說載入器會先從快取中判斷此類是否已存在,如果不存在就請示父類載入器判斷是否存在,如果直到Bootstrap類載入器都不存在該類,那麼當前類載入器就會從檔案系統中找到類檔案進行載入。

  • Bootstrap載入器:
    這個類裝載器是在JVM啟動的時候建立的。它負責裝載Java API,包含Object物件。和其他的類裝載器不同的地方在於這個裝載器是通過native code來實現的,而不是用Java程式碼。
  • 擴充套件載入器(Extension class loader): 擴充套件載入器用於載入除基本Java APIs以外擴充套件類。也用於載入各種安全擴充套件功能。
  • 系統載入器(System class loader): 如果說Bootstrap和Extension載入器用於載入JVM執行時元件,那麼系統載入器載入的則是應用程式相關的類。它會載入使用者指定的CLASSPATH裡的類。
    If the bootstrap class loader and the extension class loader load the JVM components, the system class loader loads the application classes. It loads the class in the $CLASSPATH specified by the user.
  • 使用者自定義載入器: 這個是由使用者的程式程式碼建立的類載入器。This is a class loader that an application user directly creates on the code.

Frameworks such as Web application server (WAS) use it to make Web applications and enterprise applications run independently. In other words, this guarantees the independence of applications through class loader delegation model. Such a WAS class loader structure uses a hierarchical structure that is slightly different for each WAS vendor.

像Web應用伺服器(WAS: Web Application Server)等框架通過使用使用者自定義載入器使Web應用和企業級應用可以隔離開在各自的類載入空間獨自執行。也就是說可以通過類載入器的委派模式來保證應用的獨立性。不同的WAS在自定義類載入器時會有略微不同,但都不外乎使用載入器的層次結構原理。

如果一個類載入器發現了一個未載入的類,則該類的載入和連結過程如下圖:
在這裡插入圖片描述
圖3: 類載入步驟

每一步的具體描述如下:

  • 載入(Loading): 從.class檔案中獲取類並載入到JVM記憶體空間。
  • 驗證(Verifying): 檢查讀入的結構是否符合Java語言規範以及JVM規範的描述。這是類裝載中最複雜的過程,並且花費的時間也是最長的。
    Most cases of the JVM TCK test cases are to test whether or not a verification error occurs by loading wrong classes.
    並且JVM TCK工具的大部分場景的用例也用來測試在裝載錯誤的類的時候是否會驗證失敗。
  • 準備(Preparing): 分配一個結構用來儲存類資訊,這個結構中包含了類中定義的成員變數,方法和介面的資訊。
  • 解析(Resolving): 把類常量池中所有的符號引用轉為直接引用。Change all symbolic references in the constant pool of the class to direct references.
  • 初始化(Initializing): 把類中的變數初始化成合適的值。執行靜態初始化程式,把靜態變數初始化成指定的值。 Initialize the class variables to proper values. Execute the static initializers and initialize the static fields to the configured values.
    JVM規範定義了上面的幾個任務,不過它允許具體執行的時候能夠有些靈活的變動。

執行時資料區

在這裡插入圖片描述

圖4: 執行時資料區(Runtime Data Areas)

Runtime Data Areas are the memory areas 【which is】assigned(分配) when the JVM program runs on the OS. The runtime data areas can be divided into 6 areas. Of the six, one PC Register, JVM Stack, and Native Method Stack are created for one thread. Heap, Method Area, and Runtime Constant Pool are shared by all threads.

執行時資料區域是在作業系統上執行JVM程式時分配的記憶體區域。執行時資料區域可分為6個區域。在這6個區域中,一個PC Register,JVM stack 以及Native Method Statck都是為每一個執行緒建立的,Heap,Method Area以及Runtime Constant Pool都是被所有執行緒共享的。

PC 暫存器(PC register)::

One PC (Program Counter) register exists for one thread, and is created when the thread starts. PC register has the address of a JVM instruction being executed now.

每個執行緒都會有一個PC(Program Counter)暫存器,並跟隨執行緒的啟動而建立。PC暫存器裡儲存有當前正在執行的JVM指令的地址。

JVM 棧(JVM stack):
One JVM stack exists for one thread, and is created when the thread starts. It is a stack that saves the struct (Stack Frame). The JVM just pushes or pops the stack frame to the JVM stack.
If any exception occurs, provides programmatic access to the stack trace information printed by printStackTrace(). Returns an array of stack trace elements, each representing one stack frame

每個執行緒啟動的時候,都會建立一個JVM stack。它是用來儲存棧幀(Stack Frame)。JVM只會在JVM stack上對棧幀進行push和pop的操作。如果出現了異常,使用printStackTrace()方法。可以獲取一個棧跟蹤元素陣列,每個元素表示一個棧幀。

JVM Stack per Thread

圖5: JVM棧結構

  • 1.- 棧幀(stack frame):

One stack frame is created whenever a method is executed in the JVM, and the stack frame is added to the JVM stack of the thread. When the method is ended, the stack frame is removed.

Each stack frame has the reference for local variable array, Operand stack, and runtime constant pool of a class where the method being executed belongs.

在JVM中一旦有方法執行,JVM就會為之建立一個棧幀,並把其新增到當前執行緒的JVM棧中。當方法執行結束時,棧幀也會相應的從JVM棧中移除。
Each stack frame has the reference for local variable array, Operand stack, and runtime constant pool of a class where the method being executed belongs

每個棧幀裡都包含有當前正在執行的方法所屬類的區域性變數陣列 的引用,運算元棧 的引用,以及執行時常量池 的引用

The size of local variable array and Operand stack is determined while compiling. Therefore, the size of stack frame is fixed according to the method.

區域性變數陣列的和運算元棧的大小都是在編譯時確定的。因此,一個方法的棧幀的大小也是固定不變的。

  • 2.- 區域性變數陣列(Local variable array ):
    It has an index starting from 0. 0 is the reference of a class instance where the method belongs. From 1, the parameters sent to the method are saved. After the method parameters, the local variables of the method are saved.
    這個陣列的索引從0開始。索引為0的變數表示這個方法所屬的類的例項。從1開始,首先存放的是傳給該方法的引數,在引數後面儲存的是方法的區域性變數。

  • 3 - 運算元棧(Operand stack ):

An actual workspace of a method. Each method exchanges data between the Operand stack and the local variable array, and pushes or pops other method invoke results. The necessary size of the Operand stack space can be determined during compiling. Therefore, the size of the Operand stack can also be determined during compiling.

方法實際執行的工作空間。每個方法都在運算元棧和區域性變數陣列之間交換資料,並把呼叫其它方法的結果從棧中彈或壓入。在編譯時,編譯器就能計算出運算元棧所需的記憶體大小,因此運算元棧的大小在編譯時也是確定的。

本地方法棧( Native method stack):
A stack for native code written in a language other than Java. In other words, it is a stack used to execute C/C++ codes invoked through JNI (Java Native Interface). According to the language, a C stack or C++ stack is created.
A stack for native code(不用Java語言編寫的程式碼) 。它是通過呼叫JNI(Java Native Interface Java本地介面)去執行C/C++程式碼。 According to the language,一個C堆疊或者C++堆疊會被建立。

方法區(Method area):

The method area is shared by all threads, created when the JVM starts.

It (The method area)stores runtime constant pool, field and method information, static variable,
and method bytecode for each of the classes and interfaces read by the JVM.

The method area can be implemented in various formats by JVM vendor.

Oracle Hotspot JVM calls it (指的是method area)Permanent Area or Permanent Generation (PermGen). The garbage collection for the method area is optional for each JVM vendor.

方法區是所有執行緒共享的,它是在JVM啟動的時候建立的。

它儲存所有被JVM載入的類和介面的 執行時常量池,成員變數以及方法的資訊,靜態變數以及方法的位元組碼。

JVM的提供者可以通過不同的方式來實現方法區。

在Oracle 的HotSpot JVM裡,方法區被稱為Permanent Area(永久區)或者Permanent Generation(PermGen)。
JVM規範並對方法區的垃圾回收未做強制限定,因此對於JVM vendor(提供者)來說,方法區的垃圾回收是可選操作。

執行時常量池( Runtime constant pool ):

An area that corresponds to the constant_pool table in the class file format. This area is included in the method area;

however, it plays the most core role in JVM operation. Therefore, the JVM specification separately describes its importance.

As well as the constant of each class and interface, it contains all references for methods and fields. In short, when a method or field is referred to, the JVM searches the actual address of the method or field on the memory by using the runtime constant pool.

這個區域和class檔案裡的the constant_pool table是相對應的。這個區域是包含在method area裡的。
不過,對於JVM的操作而言,它是一個核心的角色。因此在JVM規範裡特別提到了它的重要性。

除了包含每個類和介面的常量( the constant of each class and interface),它也包含了所有方法和變數的引用( all references for methods and fields.)。簡而言之,當一個方法或者變數被引用時,JVM通過執行時常量區來查詢方法或者變數在記憶體裡的實際地址(the JVM searches the actual address of the method or field on the memory by using the runtime constant pool.)。

這部分空間雖然存在於方法區內,但卻在JVM操作中扮演著舉足輕重的角色,因此JVM規範單獨把這一部分拿出來描述。除了每個類或介面中定義的常量,它還包含了所有對方法和欄位的引用。因此當需要一個方法或欄位時,JVM通過執行時常量池中的資訊從記憶體空間中來查詢其相應的實際地址。

堆(Heap):
A space that stores instances or objects, and is a target of garbage collection. This space is most frequently mentioned when discussing issues such as JVM performance. JVM vendors can determine how to configure the heap or not to collect garbage.

堆中儲存著所有的類例項或物件,而且它是垃圾回收的主要目標。當涉及到類似於JVM效能之類的問題時,當討論類似於JVM效能之類的問題時,它經常會被提及。JVM提供者可以決定劃分堆空間或者不執行垃圾回收。

現在我們再會過頭來看看之前反彙編的位元組碼:

public void add(java.lang.String);
  Code:
   0:   aload_0
   1:   getfield        #15; //Field admin:Lcom/nhn/user/UserAdmin;
   4:   aload_1
   5:   invokevirtual   #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;
   8:   pop
   9:   return

// UserService.java
…
public void add(String userName) {
    admin.addUser(userName);
}

Comparing the disassembled code and the assembly code of the x86 architecture that we sometimes see, the two have a similar format, OpCode;

however, there is a difference in that Java Bytecode does not write register name, memory addressor, or offset on the Operand.
As described before, the JVM uses stack. Therefore, it does not use register, unlike the x86 architecture that uses registers,
and it uses index numbers such as 15 and 23 instead of memory addresses since it manages the memory by itself. The 15 and 23 are the indexes of the constant pool of the current class (here, UserService class)

In short, the JVM creates a constant pool for each class, and the pool stores the reference of the actual target.

把上面的反彙編程式碼和我們平時所見的x86架構的彙編程式碼相比較,我們會發現這兩者的結構有點相似,都使用了操作碼;

不過,有一點不同的地方是Java位元組碼並不會在運算元裡寫入暫存器的名稱、記憶體地址或者偏移量。
之前已經說過,JVM用的是棧,它不會使用暫存器。和使用暫存器的x86架構不同,它自己負責記憶體的管理。它用索引例如15和23來代替實際的記憶體地址。15和23都是當前類(這裡是UserService類)的常量池裡的索引。

簡而言之,JVM為每個類建立了一個常量池,並且這個常量池裡儲存了真實物件的引用。

Each row of the disassembled code is interpreted as follows.
上面每行程式碼的解釋如下:

  • aload_0:
    Add the #0 index of the local variable array to the Operand stack. The #0 index of the local variable array is always this, the reference for the current class instance.

把區域性變數陣列中索引為#0的變數新增到運算元棧上。區域性變數陣列中索引#0所表示的變數是this,即是當前類例項物件的引用

  • getfield /#15:
    In the current class constant pool, add the #15 index to the Operand stack. UserAdmin admin field is added. Since the admin field is a class instance, a reference is added.

把當前類的常量池裡的索引為#15的變數新增到運算元棧。這裡新增的是UserAdmin的admin成員變數。因為admin變數是個類的例項,因此新增的是一個引用。

  • aload_1:

Add the #1 index of the local variable array to the Operand stack. From the #1 index of the local variable array, it is a method parameter. Therefore, the reference of String userName sent while invoking add() is added.

把區域性變數陣列裡的索引為#1的變數新增到運算元棧。本地變數陣列中從第1個位置開始的元素儲存著方法的引數。
因此,在呼叫add()方法的時候,會把userName指向的String的引用新增到運算元棧上。

  • invokevirtual /#23:
    Invoke the method corresponding to the #23 index in the current class constant pool. At this time, the reference added by using getfield and the parameter added by using aload_1 are sent to the method to invoke. When the method invocation is completed, add the return value to the Operand stack.

呼叫當前類的常量池裡的索引為#23的方法。這個時候,通過getfile新增到運算元棧上的引用和通過aload_1新增到運算元棧上的引數(parameter)都被傳給方法呼叫。當方法執行完成時,它的返回值結果會被新增到運算元棧上。

  • pop:
    Pop the return value of invoking by using invokevirtual from the Operand stack.
    You can see that the code compiled by the previous library has no return value. In short, the previous has no return value, so there was no need to pop the return value from the stack.

把通過invokevirtual方法呼叫得到的結果從運算元棧中彈出。
在前面講述中使用之前類庫時沒有返回值,也就不需要把結果從運算元棧中彈出了。

  • return: Complete the method. 方法完成。

下圖將幫助理解上面的文字解釋:
在這裡插入圖片描述
圖6: 從執行時資料區載入Java位元組碼示例

For reference, in this method, no local variable array has been changed.
So the figure above displays the changes in Operand stack only. However, in most cases, local variable array is also changed.

Data transfer between the local variable array and the Operand stack is made by using a lot of load instructions (aload, iload) and store instructions (astore, istore).

順便提一下,在這個方法裡,區域性變數陣列沒有被修改。所以上圖只顯示了運算元棧的變化。不過,大部分的情況下,區域性變數陣列也是會改變的。

區域性變數陣列和運算元棧之間的資料傳輸是使用通過大量的load指令(aload,iload)和store指令(astore,istore)來實現的。

In this figure, we have checked the brief description of the runtime constant pool and the JVM stack. When the JVM runs, each class instance will be assigned to the heap, and class information including User, UserAdmin, UserService, and String will be stored in the method area.

在這個圖裡,我們簡單驗證了執行時常量池和JVM棧的描述。當JVM執行的時候,每個類的例項都會在堆上進行分配,User,UserAdmin,UserService以及String等類的資訊都會被儲存在方法區。

執行引擎

The bytecode that is assigned to the runtime data areas in the JVM via class loader is executed by the execution engine. The execution engine reads the Java Bytecode in the unit of instruction. It is like a CPU executing the machine command one by one. Each command of the bytecode consists of a 1-byte OpCode and additional Operand. The execution engine gets one OpCode and execute task with the Operand, and then executes the next OpCode.

通過類裝載器裝載的,被分配到JVM的執行時資料區的位元組碼會被執行引擎執行。執行引擎在指令單元讀取Java位元組碼。(它就像一個CPU一樣,一條一條地執行機器指令。)每個位元組碼指令都由一個1位元組的操作碼和附加的運算元組成。執行引擎取得一個操作碼,然後根據運算元來執行任務,完成後就繼續執行下一條操作碼。

But the Java Bytecode is written in a language that a human can understand, rather than in the language that the machine directly executes. Therefore, the execution engine must change the bytecode to the language that can be executed by the machine in the JVM. The bytecode can be changed to the suitable language in one of two ways.

儘管如此,Java位元組碼還是以一種可以理解的語言編寫的,而不是用機器可以直接執行的語言。因此,JVM的執行引擎必須把位元組碼轉換成直接被機器(the machine in the JVM)執行的機器碼。位元組碼可以通過以下兩種方式轉換成合適的語言。

ps:https://www.cnblogs.com/chanshuyi/p/jvm_serial_04_from_source_code_to_machine_code.html 這邊有更加詳細的介紹
當原始碼轉化為位元組碼之後,其實要執行程式,有兩種選擇。一種是使用 Java 直譯器解釋執行位元組碼,另一種則是使用 JIT 編譯器將位元組碼轉化為本地機器程式碼。

  • 直譯器(Interpreter):
    Reads, interprets and executes the bytecode instructions one by one. As it interprets and executes instructions one by one, it can quickly interpret one bytecode, but slowly executes the interpreted result. This is the disadvantage of the interpret language. The 'language' called Bytecode basically runs like an interpreter.

一條一條地讀取,解釋並且執行位元組碼指令。因為它一條一條地解釋和執行指令,所以它可以很快地解釋位元組碼,但是執行起來會比較慢。這是解釋執行的語言的一個缺點。位元組碼這種“語言”基本來說是解釋執行的。

  • 即時編譯器(JIT: Just-In-Time):
    The JIT compiler has been introduced to compensate for the disadvantages of the interpreter. The execution engine runs as an interpreter first, and at the appropriate time, the JIT compiler compiles the entire bytecode to change it to native code. After that, the execution engine no longer interprets the method, but directly executes using native code. Execution in native code is much faster than interpreting instructions one by one. The compiled code can be executed quickly since the native code is stored in the cache.

即時編譯器被引入用來彌補直譯器的缺點。執行引擎首先按照解釋執行的方式來執行,然後在合適的時候,即時編譯器把整段位元組碼編譯成原生程式碼。然後,執行引擎就沒有必要再去解釋執行方法了,它可以直接通過原生程式碼去執行它。執行原生程式碼比一條一條進行解釋執行的速度快很多。編譯後的程式碼可以執行的很快,因為原生程式碼是儲存在快取裡的。

However, it takes more time for JIT compiler to compile the code than for the interpreter to interpret the code one by one. Therefore, if the code is to be executed just once, it is better to interpret it instead of compiling. Therefore, the JVMs that use the JIT compiler internally check how frequently the method is executed and compile the method only when the frequency is higher than a certain level.

不過,用JIT編譯器來編譯程式碼所花的時間要比用直譯器去一條條解釋執行花的時間要多。因此,如果程式碼只被執行一次的話,那麼最好還是解釋執行而不是編譯後再執行。因此,內建了JIT編譯器的JVM都會檢查方法的執行頻率,如果一個方法的執行頻率超過一個特定的值的話,那麼這個方法就會被編譯成原生程式碼。

在這裡插入圖片描述
圖7: Java編譯器和即時編譯器

How the execution engine runs is not defined in the JVM specifications. Therefore, JVM vendors improve their execution engines using various techniques, and introduce various types of JIT compilers.

JVM規範沒有定義執行引擎該如何去執行。因此,JVM的提供者通過使用不同的技術以及不同型別的JIT編譯器來提高執行引擎的效率。

Most JIT compilers run as shown in the figure below:

大部分的即時編譯器執行流程如下圖:
在這裡插入圖片描述
圖8: 即時編譯器

The JIT compiler converts the bytecode to an intermediate-level expression, IR (Intermediate Representation), to execute optimization, and then converts the expression to native code.

即時編譯器先把位元組碼轉為一種中間形式的表示式(IR: Itermediate Representation),來進行優化,然後再把這種表示轉換成原生程式碼

Oracle Hotspot VM uses a JIT compiler called Hotspot Compiler. It is called Hotspot because Hotspot Compiler searches the 'Hotspot' that requires compiling with the highest priority through profiling, and then it compiles the hotspot to native code. If the method that has the bytecode compiled is no longer frequently invoked, in other words, if the method is not the hotspot any more, the Hotspot VM removes the native code from the cache and runs in interpreter mode. The Hotspot VM is divided into the Server VM and the Client VM, and the two VMs use different JIT compilers.

Oracle Hotspot VM使用一種叫做Hotspot Compiler 的JIT編譯器。它之所以被稱作”Hotspot“是因為Hotspot Compilerr會根據剖析找到具有更高編譯優先順序的熱點程式碼,然後把熱點程式碼編譯成原生程式碼。如果已經被編譯成原生程式碼的位元組碼不再被頻繁呼叫了,換句話說,這個方法不再是熱點了,Hotspot VM會把這些原生程式碼從快取中刪除並對其再次使用直譯器模式執行。Hotspot VM分為Server VM和Client VM兩種,這兩種VM使用不同的JIT編譯器。

在這裡插入圖片描述
圖9: Hotspot ClientVM 和Server VM

The client VM and the server VM use an identical runtime; however, they use different JIT compilers, as shown in the above figure.
Advanced Dynamic Optimizing Compiler used by the server VM uses more complex and diverse (多樣的)performance optimization techniques.

Client VM 和Server VM使用完全相同的執行時,不過如上圖所示,它們所使用的JIT編譯器是不同的。Server VM用的是更高階的動態優化編譯器,這個編譯器使用了更加複雜並且更多種類的效能優化技術。

IBM JVM has introduced AOT (Ahead-Of-Time) Compiler from IBM JDK 6 as well as the JIT compiler. This means that many JVMs share the native code (which is )compiled through the shared cache.
In short, the code that has been already compiled through the AOT compiler can be used by another JVM without compiling. In addition, IBM JVM provides a fast way of execution by pre-compiling code to JXE (Java EXecutable) file format using the AOT compiler.

IBM 在IBM JDK 6裡不僅引入了JIT編譯器,它同時還引入了AOT(Ahead-Of-Time)編譯器。它使得多個JVM可以通過共享快取來共享編譯過的原生程式碼。
簡而言之,通過AOT編譯器編譯過的程式碼可以直接被其他JVM使用。除此之外,IBM JVM通過使用AOT編譯器通過提前把程式碼編譯器成JXE(Java EXecutable)檔案格式從而提供了一種快速執行程式碼的方式。

Most Java performance improvement is accomplished by improving the execution engine. As well as the JIT compiler, various optimization techniques are being introduced so the JVM performance can be continuously improved. The biggest difference between the initial JVM and the latest JVM is the execution engine.

大多數的Java效能提升都是通過優化執行引擎的效能實現的。像即時編譯等各種優化技術被不斷的引入,從而使得JVM效能得到了持續的優化和提升。老舊的JVM與最新的JVM之間最大的差異其實就來自於執行引擎。

Hotspot compiler has been introduced to Oracle Hotspot VM from version 1.3, and JIT compiler has been introduced to Dalvik VM from Android 2.2.

Hotspot編譯器從Java 1.3開始便引入到了Oracle Hotspot VM中,而即時編譯器從Android 2.2開始便被引入到了Android Dalvik VM中。

Note
The technique in which an intermediate language such as bytecode is introduced, the VM executes the bytecode, and the JIT compiler improves the performance of JVM is also commonly used in other languages that have introduced intermediate languages. For Microsoft's .Net, CLR (Common Language Runtime), a kind of VM, executes a kind of bytecode, called CIL (Common Intermediate Language). CLR provides the AOT compiler as well as the JIT compiler. Therefore, if source code is written in C# or VB.NET and compiled, the compiler creates CIL and the CIL is executed on the CLR with the JIT compiler. The CLR uses the garbage collection and runs as a stack machine like the JVM.

註釋
引入一種中間語言,例如位元組碼,虛擬機器執行位元組碼,並且通過JIT編譯器來提升JVM的效能的這種技術以及廣泛應用在使用中間語言的程式語言上。例如微軟的.Net,CLR(Common Language Runtime 公共語言執行時),也是一種VM,它執行一種被稱作CIL(Common Intermediate Language)的位元組碼。CLR提供了AOT編譯器和JIT編譯器。因此,用C#或者VB.NET編寫的原始碼被編譯後,編譯器會生成CIL並且CIL會執行在有JIT編譯器的CLR上。CLR和JVM相似,它也有垃圾回收機制,並且也是基於棧執行。

Java 虛擬機器規範,Java SE 第7版

2011年7月28日,Oracle釋出了Java SE的第7個版本,並且把JVM規也更新到了相應的版本。在1999年釋出《The Java Virtual Machine Specification,Second Edition》後,Oracle花了12年來發布這個更新的版本。這個更新的版本包含了這12年來累積的眾多變化以及修改,並且更加細緻地對規範進行了描述。此外,它還反映了《The Java Language Specificaion,Java SE 7 Edition》裡的內容。主要的變化總結如下:

  • 來自Java SE 5.0裡的泛型,支援可變引數的方法
  • 從Java SE 6以來,位元組碼校驗的處理技術所發生的改變
  • 新增invokedynamic指令以及class檔案對於該指令的支援
  • 刪除了關於Java語言概念的內容,並且指引讀者去參考Java語言規範
  • 刪除關於Java執行緒和鎖的描述,並且把它們移到Java語言規範裡
    最大的改變是新增了invokedynamic指令。也就是說JVM的內部指令集做了修改,使得JVM開始支援動態型別的語言,這種語言的型別不是固定的,例如指令碼語言以及來自Java SE 7裡的Java語言。之前沒有被用到的操作碼186被分配給新指令invokedynamic,而且class檔案格式裡也新增了新的內容來支援invokedynamic指令。

Java SE 7的編譯器生成的class檔案的版本號是51.0。Java SE 6的是50.0。class檔案的格式變動比較大,因此,51.0版本的class檔案不能夠在Java SE 6的虛擬機器上執行。

儘管有了這麼多的變動,但是Java方法的65535位元組的限制還是沒有被去掉。除非class檔案的格式徹底改變,否者這個限制將來也是不可能去掉的。

值得說明的是,Oracle Java SE 7 VM支援G1這種新的垃圾回收機制,不過,它被限制在Oracle JVM上,因此,JVM本身對於垃圾回收的實現不做任何限制。也因此,在JVM規範裡沒有對它進行描述。

String in switch Statements

Java SE 7 adds various grammars and features. However, compared to the various changes in language of Java SE 7, there are not so many changes in the JVM. So, how can the new features of the Java SE 7 be implemented? We will see how String in switch Statements (a function to add a string to a switch() statement as a comparison) has been implemented in Java SE 7 by disassembling it.

For example, the following code has been written.

Java SE 7裡新增了很多新的語法和特性。不過,在Java SE 7的版本里,相對於語言本身而言,JVM沒有多少的改變。那麼,這些新的語言特性是怎麼來實現的呢?我們通過反彙編的方式來看看switch語句裡的String(把字串作為switch()語句的比較物件)是怎麼實現的?

例如,下面的程式碼:

// SwitchTest
public class SwitchTest {
    public int doSwitch(String str) {
        switch (str) {
        case "abc":        return 1;
        case "123":        return 2;
        default:         return 0;
        }
    }
}

Since it is a new function of Java SE 7, it cannot be compiled using the Java compiler for Java SE 6 or lower versions. Compile it using the javac of Java SE 7. The following screen is the compiling result printed by using javap –c.
因為這是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

A significantly longer bytecode than the Java source code has been created. First, you can see that lookupswitch instruction has been used for switch() statement in Java bytecode.
However, two lookupswitch instructions have been used, not the one lookupswitch instruction.
When disassembling the case in which int has been added to switch() statement, only one lookupswitch instruction has been used.
This means that the switch() statement has been divided into two statements to process the string.
See the annotation of the #5, #39, and #53 byte instructions to see how the switch() statement has processed the string.

生成的位元組碼的長度比Java原始碼長多了。
首先,你可以看到位元組碼裡用lookupswitch指令來實現switch()語句。
不過,這裡使用了兩個lookupswitch指令,而不是一個。
如果反編譯的是針對Int的switch()語句的話,位元組碼裡只會使用一個lookupswitch指令。
也就是說,針對string的switch語句被分成用兩個語句來實現。
請參閱#5、#39和#53位元組指令的註釋,以瞭解switch()語句如何處理字串。

In the #5 and #8 byte, first, hashCode() method has been executed and switch(int) has been executed by using the result of executing hashCode() method.

在#5和#8位元組處,首先是呼叫了hashCode()方法,然後它作為引數呼叫了switch(int)。

In the braces of the lookupswitch instruction, branch is made to the different location according to the hashCode result value. String "abc" is hashCode result value 96354, and is moved to #36 byte. String "123" is hashCode result value 48690, and is moved to #50 byte.

在lookupswitch的指令裡,根據hashCode的結果進行不同的分支跳轉。字串“abc"的hashCode是96354,它會跳轉到#36處。字串”123“的hashCode是48690,它會跳轉到#50處。

In the #36, #37, #39, and #42 bytes, you can see that the value of the str variable received as an argument is compared using the String "abc" and the equals() method. If the results are identical, '0' is inserted to the #3 index of the local variable array, and the string is moved to the #61 byte.

在第#36,#37,#39,以及#42位元組的地方,你可以看見str引數被equals()方法來和字串“abc”進行比較。如果比較的結果是相等的話,‘0’會被放入到區域性變數陣列的索引為#3的位置,然後跳轉到第#61位元組。

In this way, in the #50, #51, #53, and #56 bytes, you can see that the value of the str variable received as an argument is compared by using the String "123" and the equals() method. If the results are identical, '1' is inserted to the #3 index of the local variable array and the string is moved to the #61 byte.

在第#50,#51,#53,以及#56位元組的地方,你可以看見str引數被equals()方法來和字串“123”進行比較。如果比較的結果是相等的話,'1'會被放入到區域性變數陣列的索引為#3的位置,然後跳轉到第#61位元組。

In the #61 and #62 bytes, the value of the #3 index of the local variable array, i.e., '0', '1', or any other value, is lookupswitched and branched.

在第#61和#62位元組的地方,區域性變數陣列裡索引為#3的值,這裡是'0',‘1’或者其他的值,被lookupswitch用來進行搜尋並進行相應的分支跳轉。

In other words, in Java code, the value of the str variable received as the switch() argument is compared using the hashCode() method and the equals() method. With the result int value, switch() is executed.

換句話來說,在Java程式碼裡的用來作為switch()的引數的字串str變數是通過hashCode()和equals()方法來進行比較,然後根據比較的結果,來執行swtich()語句。

In this result, the compiled bytecode is not different from the previous JVM specifications. The new feature of Java SE 7, String in switch is processed by the Java compiler, not by the JVM itself. In this way, other new features of Java SE 7 will also be processed by the Java compiler.

在這個結果裡,編譯後的位元組碼和之前版本的JVM規範沒有不相容的地方。Java SE 7的這個用字串作為switch引數的特性是通過Java編譯器來處理的,而不是通過JVM來支援的。通過這種方式還可以把其他的Java SE 7的新特性也通過Java編譯器來實現。

結束語

我不認為為了使用好Java必須去了解Java底層的實現。許多沒有深入理解JVM的開發者也開發出了很多非常好的應用和類庫。不過,如果你更加理解JVM的話,你就會更加理解Java,這樣你會有助於你處理類似於我們前面的案例中的問題。

除了這篇文章裡提到的,JVM還是用了其他的很多特性和技術。JVM規範提供了是一種擴充套件性很強的規範,這樣就使得JVM的提供者可以選擇更多的技術來提高效能。值得特別說明的一點是,垃圾回收技術被大多數使用虛擬機器的語言所使用。不過,由於這個已經在很多地方有更加專業的研究,我這篇文章就沒有對它進行深入講解了。

相關文章