JVM 內部原理(五)— 基本概念之 Java 虛擬機器官方規範文件,第 7 版

Richaaaard發表於2016-12-19

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

2016.05 深入理解java虛擬機器

結束

相關文章