JVM 內部原理(三)— 基本概念之類檔案格式

Richaaaard發表於2016-12-19

JVM 內部原理(三)— 基本概念之類檔案格式

介紹

版本: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 版

    • 分支語句中的字串
  • 總結

內容

類檔案格式(Class File Format)

在解釋 Java 類檔案格式之前,讓我們先檢視一個 Java Web 應用程式經常出現的狀況。

症狀

當在 Tomcat 上執行 JSP 時,JSP 並沒有執行,而是出現一下錯誤。

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

原因

以上的錯誤訊息提示會因為 Web 應用伺服器不同而有些許差異,但有有樣事情是一樣的:那就是 65535 位元組的限制。65535 位元組的限制是由於 JVM 的限制規定,那就是 一個方法的大小不可以超過 65535 位元組

我會解釋 65535 位元組的限制以及為什麼會有這樣的限制。

Java 位元組碼使用的 branch/jump 指令是 “goto” 和 “jsr” 。

1 goto [branchbyte1] [branchbyte2]
2 jsr [branchbyte1] [branchbyte2]

兩者都接收一個 2 位元組有符號的 branch 偏移量作為他們的運算元,這樣它可以擴充套件到最大 65535 索引。然而,為了支援足夠的 branch ,Java 位元組碼提供了 “goto_w” 和 “jsr_w” 接收 4 位元組有符號的 branch 偏移量。

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

有了這兩條指令,branch 可以提供超過 65535 的地址索引,這樣就可以解決對於 Java 方法 65535 位元組的大小的限制。然而,由於 Java 類檔案格式其他諸多方面的限制,Java 方法仍然無法超過 65535 位元組。為了解釋其他的這些限制,我會通過類檔案格式來進行簡單的說明。

一個 Java 類檔案結構如下:

 1 ClassFile {
 2     u4 magic; 
 3     u2 minor_version;
 4     u2 major_version;
 5     u2 constant_pool_count;
 6     cp_info constant_pool[constant_pool_count-1];
 7     u2 access_flags;
 8     u2 this_class;
 9     u2 super_class;
10     u2 interfaces_count;
11     u2 interfaces[interfaces_count];
12     u2 fields_count;
13     field_info fields[fields_count];
14     u2 methods_count;
15     method_info methods[methods_count];
16     u2 attributes_count;
17     attribute_info attributes[attributes_count];}

首 16 位元組 UserService.class 檔案反編譯後的十六進位制顯示如下:

ca fe ba be 00 00 00 32 00 28 07 00 02 01 00 1b

將這個值與類檔案格式一起檢視。

  • magic:類檔案的前 4 位元組的內容是魔數(magic number)。它的值已經預先指定好,用來區分 Java 類檔案。在以上十六進位制中,它的值始終是 0xCAFEBABE 。簡而言之,當檔案的前 4 位元組是 0xCAFEBABE 時,那麼它就是這個 Java 類檔案。

  • minor_version,major_version:後面接著的 4 位元組表示類的版本。UserService.class 檔案這個值是 0x00000032 ,類的版本是 50.0 。由 JDK1.6 編譯的類檔案版本是 50.0 ,有 JDK1.5 編譯的類檔案版本是 49.0 。JVM 必須對比它低版本編譯的類檔案保持向後相容。另一方面,當更高版本的類檔案在低版本的 JVM 中執行是,java.lang.UnsupportedClassVersionError 就會出現。

  • constant_pool_count,constant_pool[]:在版本資訊之後,是類的型別常量池資訊。它的資訊包括了執行時常量池區域,我們稍後對此進行解釋。當裝載類檔案時,JVM 將常量池(constant_pool)的資訊儲存在方法區(method area)的執行時常量池區(Runtime Constant Pool area)。因為類檔案 UserService.class 的 constant_pool_count 值為 0x0028 ,那麼 constant_pool 有(40-1)個索引,即 39 個索引。

  • access_flags:這個標誌表示類的修飾符資訊;換句話說,它表示 public、final、abstract、或是否為 interface 。

  • this_class,super_class:類對應的 this 和 super 在常量池(constant_pool)中的索引。

  • interfaces_count,interfaces[]:類實現介面的數量在常量池(constant_pool)中的索引,以及每個介面的索引。

  • fields_count,fields[]:類中欄位的數量以及欄位的資訊。欄位的資訊包括欄位名、型別資訊、修飾符、以及常量池的索引值。

  • methods_count,methods[]:類中方法的數量以及類方法的資訊。方法資訊包括方法名、引數的型別及數量、返回型別、修飾符、常量池的索引值、方法執行的程式碼和異常資訊。

  • attributes_count,attributes[]:attribute_info 的結構包括多個不同的 attributes 。供 field_info、method_info 和 attribute_info 使用。

javap 反編譯程式可以將類檔案格式反編譯為程式設計師可讀的形式。使用 “javap -verbose” 分析 UserService.class 類,得到以下內容。

 1 Compiled from "UserService.java"
 2  
 3 public class com.nhn.service.UserService extends java.lang.Object
 4   SourceFile: "UserService.java"
 5   minor version: 0
 6   major version: 50
 7   Constant pool:const #1 = class        #2;     //  com/nhn/service/UserService
 8 const #2 = Asciz        com/nhn/service/UserService;
 9 const #3 = class        #4;     //  java/lang/Object
10 const #4 = Asciz        java/lang/Object;
11 const #5 = Asciz        admin;
12 const #6 = Asciz        Lcom/nhn/user/UserAdmin;;// … omitted - constant pool continued …
13  
14 {
15 // … omitted - method information …
16  
17 public void add(java.lang.String);
18   Code:
19    Stack=2, Locals=2, Args_size=2
20    0:   aload_0
21    1:   getfield        #15; //Field admin:Lcom/nhn/user/UserAdmin;
22    4:   aload_1
23    5:   invokevirtual   #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;
24    8:   pop
25    9:   return  LineNumberTable:
26    line 14: 0
27    line 15: 9  LocalVariableTable:
28    Start  Length  Slot  Name   Signature
29    0      10      0    this       Lcom/nhn/service/UserService;
30    0      10      1    userName       Ljava/lang/String; // … Omitted - Other method information …
31 }

由於內容太長,這裡只提取了部分資訊。完整的內容提供了各種資訊,包括常量池和每個方法體的內容。

方法 65535 位元組的大小限制與 method_info struct 的內容相關。結構 method_info struct 裡有程式碼(Code),行號表(LineNumberTable)和本地變數表(LocalVariableTable)屬性,如上所示。所有程式碼內包括如行號表(LineNumberTable),本地變數表(LocalVariableTable)和異常表(exception_table)的屬性值都是 2 位元組的。因此,方法的大小不能超過行號表(LineNumberTable),本地變數表(LocalVariableTable)和異常表(exception_table)的長度,即 65535 位元組。

許多人抱怨過這個方法大小的限制,JVM 規範上解釋說 “可能以後會擴充套件” 。然而,到目前為止還沒有明顯的行動。考慮到 JVM 規範的特點通常在方法區載入類檔案的內容相同,既要向後保持相容又要能擴充套件方法的大小是十分困難的

“如果是因為 Java 編譯器的錯誤導致類檔案不正確會怎麼樣?或者,如果因為網路傳輸或檔案拷貝過程出現錯誤,類檔案會被破壞?”

為了應對這種情形,Java 類裝載器(class loader)檢查過程是非常嚴格的。JVM 規範明確規定了這個過程。

注意

我們如何驗證 JVM 成功執行了類檔案的驗證過程?如何驗證來自不同 JVM 提供商的各種各樣的 JVM 是否滿足 JVM 規範的要求?為了驗證,Oracle 提供了一個測試工具,TCK(Technology Compatibility Kit)。TCK 通過執行上萬個測試來驗證 JVM 規範是否能得到滿足,包括很多不正確的類檔案。如果能通過 TCK 的測試,那一個 JVM 才能稱為 JVM 。

和 TCK 類似,JCP(Java Community Process; http://jcp.org)會提議新的 Java 技術文件以及 Java 語言規範。對於 JCP 來說,一個文件規範,參考實現,JSR(Java Specification Request)TCK 必須要完成以滿足 JSR 。想要使用以 JSR 形式提議的新 Java 技術的使用者需要遵守 RI 提供方的許可,或者直接實現它並用 TCK 對實現進行測試。

參考

參考來源:

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

2016.05 深入理解java虛擬機器

結束

相關文章