天天看點

[譯]深入了解JVM Understanding JVM Internals

轉載:

英文原版位址: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 bytecode)
      • 現象
      • 問題分析
    • 類檔案格式
      • 現象
      • 問題分析
    • JVM 結構
    • 類加載
    • 運作時資料區
    • 執行引擎
  • Java 虛拟機規範,Java SE 第7版
    • String in switch Statements
    • 結束語

正文

每個使用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代碼執行過程

類裝載器負責裝載編譯後的位元組碼,并加載到運作時資料區(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()方法。可以擷取一個棧跟蹤元素數組,每個元素表示一個棧幀。

圖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都會檢查方法的執行頻率,如果一個方法的執行頻率超過一個特定的值的話,那麼這個方法就會被編譯成本地代碼。

[譯]深入了解JVM Understanding JVM Internals

圖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編譯器。

[譯]深入了解JVM Understanding JVM Internals

圖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的提供者可以選擇更多的技術來提高性能。值得特别說明的一點是,垃圾回收技術被大多數使用虛拟機的語言所使用。不過,由于這個已經在很多地方有更加專業的研究,我這篇文章就沒有對它進行深入講解了。

jvm