JVM內幕:Java虛擬機器詳解

ImportNew發表於2016-02-06

這篇文章解釋了Java 虛擬機器(JVM)的內部架構。下圖顯示了遵守 Java SE 7 規範的典型的 JVM 核心內部元件。

JVM內幕:Java虛擬機器詳解

上圖顯示的元件分兩個章節解釋。第一章討論針對每個執行緒建立的元件,第二章節討論了執行緒無關元件。

  • 執行緒
    • JVM 系統執行緒
    • 每個執行緒相關的
    • 程式計數器
    • 本地棧
    • 棧限制
    • 棧幀
    • 區域性變數陣列
    • 運算元棧
    • 動態連結
  • 執行緒共享
    • 記憶體管理
    • 非堆記憶體
    • 即時編譯
    • 方法區
    • 類檔案結構
    • 類載入器
    • 更快的類載入
    • 方法區在哪裡
    • 類載入器參考
    • 執行時常量池
    • 異常表
    • 符號表
    • Interned 字串

執行緒

這裡所說的執行緒指程式執行過程中的一個執行緒實體。JVM 允許一個應用併發執行多個執行緒。Hotspot JVM 中的 Java 執行緒與原生作業系統執行緒有直接的對映關係。當執行緒本地儲存、緩衝區分配、同步物件、棧、程式計數器等準備好以後,就會建立一個作業系統原生執行緒。Java 執行緒結束,原生執行緒隨之被回收。作業系統負責排程所有執行緒,並把它們分配到任何可用的 CPU 上。當原生執行緒初始化完畢,就會呼叫 Java 執行緒的 run() 方法。run() 返回時,被處理未捕獲異常,原生執行緒將確認由於它的結束是否要終止 JVM 程式(比如這個執行緒是最後一個非守護執行緒)。當執行緒結束時,會釋放原生執行緒和 Java 執行緒的所有資源。

JVM 系統執行緒

如果使用 jconsole 或者其它偵錯程式,你會看到很多執行緒在後臺執行。這些後臺執行緒與觸發 public static void main(String[]) 函式的主執行緒以及主執行緒建立的其他執行緒一起執行。Hotspot JVM 後臺執行的系統執行緒主要有下面幾個:

虛擬機器執行緒(VM thread) 這個執行緒等待 JVM 到達安全點操作出現。這些操作必須要在獨立的執行緒裡執行,因為當堆修改無法進行時,執行緒都需要 JVM 位於安全點。這些操作的型別有:stop-the-world 垃圾回收、執行緒棧 dump、執行緒暫停、執行緒偏向鎖(biased locking)解除。
週期性任務執行緒 這執行緒負責定時器事件(也就是中斷),用來排程週期性操作的執行。
GC 執行緒 這些執行緒支援 JVM 中不同的垃圾回收活動。
編譯器執行緒 這些執行緒在執行時將位元組碼動態編譯成本地平臺相關的機器碼。
訊號分發執行緒 這個執行緒接收傳送到 JVM 的訊號並呼叫適當的 JVM 方法處理。

執行緒相關元件

每個執行的執行緒都包含下面這些元件:

程式計數器(PC)

PC 指當前指令(或操作碼)的地址,本地指令除外。如果當前方法是 native 方法,那麼PC 的值為 undefined。所有的 CPU 都有一個 PC,典型狀態下,每執行一條指令 PC 都會自增,因此 PC 儲存了指向下一條要被執行的指令地址。JVM 用 PC 來跟蹤指令執行的位置,PC 將實際上是指向方法區(Method Area)的一個記憶體地址。

棧(Stack)

每個執行緒擁有自己的棧,棧包含每個方法執行的棧幀。棧是一個後進先出(LIFO)的資料結構,因此當前執行的方法在棧的頂部。每次方法呼叫時,一個新的棧幀建立並壓棧到棧頂。當方法正常返回或丟擲未捕獲的異常時,棧幀就會出棧。除了棧幀的壓棧和出棧,棧不能被直接操作。所以可以在堆上分配棧幀,並且不需要連續記憶體。

Native棧

並非所有的 JVM 實現都支援本地(native)方法,那些提供支援的 JVM 一般都會為每個執行緒建立本地方法棧。如果 JVM 用 C-linkage 模型實現 JNI(Java Native Invocation),那麼本地棧就是一個 C 的棧。在這種情況下,本地方法棧的引數順序、返回值和典型的 C 程式相同。本地方法一般來說可以(依賴 JVM 的實現)反過來呼叫 JVM 中的 Java 方法。這種 native 方法呼叫 Java 會發生在棧(一般是 Java 棧)上;執行緒將離開本地方法棧,並在 Java 棧上開闢一個新的棧幀。

棧的限制

棧可以是動態分配也可以固定大小。如果執行緒請求一個超過允許範圍的空間,就會丟擲一個StackOverflowError。如果執行緒需要一個新的棧幀,但是沒有足夠的記憶體可以分配,就會丟擲一個 OutOfMemoryError。

棧幀(Frame)

每次方法呼叫都會新建一個新的棧幀並把它壓棧到棧頂。當方法正常返回或者呼叫過程中丟擲未捕獲的異常時,棧幀將出棧。更多關於異常處理的細節,可以參考下面的異常資訊表章節。

每個棧幀包含:

  • 區域性變數陣列
  • 返回值
  • 運算元棧
  • 類當前方法的執行時常量池引用

區域性變數陣列

區域性變數陣列包含了方法執行過程中的所有變數,包括 this 引用、所有方法引數、其他區域性變數。對於類方法(也就是靜態方法),方法引數從下標 0 開始,對於物件方法,位置0保留為 this。

有下面這些區域性變數:

  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

除了 long 和 double 型別以外,所有的變數型別都佔用區域性變數陣列的一個位置。long 和 double 需要佔用區域性變數陣列兩個連續的位置,因為它們是 64 位雙精度,其它型別都是 32 位單精度。

運算元棧

運算元棧在執行位元組碼指令過程中被用到,這種方式類似於原生 CPU 暫存器。大部分 JVM 位元組碼把時間花費在運算元棧的操作上:入棧、出棧、複製、交換、產生消費變數的操作。因此,區域性變數陣列和運算元棧之間的交換變數指令操作通過位元組碼頻繁執行。比如,一個簡單的變數初始化語句將產生兩條跟運算元棧互動的位元組碼。

int i;

被編譯成下面的位元組碼:

0:    iconst_0    // Push 0 to top of the operand stack
1:    istore_1    // Pop value from top of operand stack and store as local variable 1

更多關於區域性變數陣列、運算元棧和執行時常量池之間互動的詳細資訊,可以在類檔案結構部分找到。

動態連結

每個棧幀都有一個執行時常量池的引用。這個引用指向棧幀當前執行方法所在類的常量池。通過這個引用支援動態連結(dynamic linking)。

C/C++ 程式碼一般被編譯成物件檔案,然後多個物件檔案被連結到一起產生可執行檔案或者 dll。在連結階段,每個物件檔案的符號引用被替換成了最終執行檔案的相對偏移記憶體地址。在 Java中,連結階段是執行時動態完成的。

當 Java 類檔案編譯時,所有變數和方法的引用都被當做符號引用儲存在這個類的常量池中。符號引用是一個邏輯引用,實際上並不指向實體記憶體地址。JVM 可以選擇符號引用解析的時機,一種是當類檔案載入並校驗通過後,這種解析方式被稱為飢餓方式。另外一種是符號引用在第一次使用的時候被解析,這種解析方式稱為惰性方式。無論如何 ,JVM 必須要在第一次使用符號引用時完成解析並丟擲可能發生的解析錯誤。繫結是將物件域、方法、類的符號引用替換為直接引用的過程。繫結只會發生一次。一旦繫結,符號引用會被完全替換。如果一個類的符號引用還沒有被解析,那麼就會載入這個類。每個直接引用都被儲存為相對於儲存結構(與執行時變數或方法的位置相關聯的)偏移量。

執行緒間共享

堆被用來在執行時分配類例項、陣列。不能在棧上儲存陣列和物件。因為棧幀被設計為建立以後無法調整大小。棧幀只儲存指向堆中物件或陣列的引用。與區域性變數陣列(每個棧幀中的)中的原始型別和引用型別不同,物件總是儲存在堆上以便在方法結束時不會被移除。物件只能由垃圾回收器移除。

為了支援垃圾回收機制,堆被分為了下面三個區域:

  • 新生代
    • 經常被分為 Eden 和 Survivor
  • 老年代
  • 永久代

記憶體管理

物件和陣列永遠不會顯式回收,而是由垃圾回收器自動回收。通常,過程是這樣的:

  1. 新的物件和陣列被建立並放入老年代。
  2. Minor垃圾回收將發生在新生代。依舊存活的物件將從 eden 區移到 survivor 區。
  3. Major垃圾回收一般會導致應用程式暫停,它將在三個區內移動物件。仍然存活的物件將被從新生代移動到老年代。
  4. 每次進行老年代回收時也會進行永久代回收。它們之中任何一個變滿時,都會進行回收。

非堆記憶體

非堆記憶體指的是那些邏輯上屬於 JVM 一部分物件,但實際上不在堆上建立。

非堆記憶體包括:

  • 永久代,包括:
    • 方法區
    • 駐留字串(interned strings)
  • 程式碼快取(Code Cache):用於編譯和儲存那些被 JIT 編譯器編譯成原生程式碼的方法。

即時編譯(JIT)

Java 位元組碼是解釋執行的,但是沒有直接在 JVM 宿主執行原生程式碼快。為了提高效能,Oracle Hotspot 虛擬機器會找到執行最頻繁的位元組碼片段並把它們編譯成原生機器碼。編譯出的原生機器碼被儲存在非堆記憶體的程式碼快取中。通過這種方法,Hotspot 虛擬機器將權衡下面兩種時間消耗:將位元組碼編譯成原生程式碼需要的額外時間和解釋執行位元組碼消耗更多的時間。

方法區

方法區儲存了每個類的資訊,比如:

  • Classloader 引用
  • 執行時常量池
    • 數值型常量
    • 欄位引用
    • 方法引用
    • 屬性
  • 欄位資料
    • 針對每個欄位的資訊
      • 欄位名
      • 型別
      • 修飾符
      • 屬性(Attribute)
  • 方法資料
    • 每個方法
      • 方法名
      • 返回值型別
      • 引數型別(按順序)
      • 修飾符
      • 屬性
  • 方法程式碼
    • 每個方法
      • 位元組碼
      • 運算元棧大小
      • 區域性變數大小
      • 區域性變數表
      • 異常表
      • 每個異常處理器
      • 開始點
      • 結束點
      • 異常處理程式碼的程式計數器(PC)偏移量
      • 被捕獲的異常類對應的常量池下標

所有執行緒共享同一個方法區,因此訪問方法區資料的和動態連結的程式必須執行緒安全。如果兩個執行緒試圖訪問一個還未載入的類的欄位或方法,必須只載入一次,而且兩個執行緒必須等它載入完畢才能繼續執行。

類檔案結構

一個編譯後的類檔案包含下面的結構:

ClassFile {
    u4            magic;
    u2            minor_version;
    u2            major_version;
    u2            constant_pool_count;
    cp_info        contant_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];
}
Magic, minor_version, major_version 類檔案的版本資訊和用於編譯這個類的 JDK 版本。
constant_pool 類似於符號表,儘管它包含更多資料。下面有更多的詳細描述。
access_flags 提供這個類的描述符列表。
this_class 提供這個類全名的常量池(constant_pool)索引,比如org/jamesdbloom/foo/Bar。
super_class 提供這個類的父類符號引用的常量池索引。
interfaces 指向常量池的索引陣列,提供那些被實現的介面的符號引用。
fields 提供每個欄位完整描述的常量池索引陣列。
methods 指向constant_pool的索引陣列,用於表示每個方法簽名的完整描述。如果這個方法不是抽象方法也不是 native 方法,那麼就會顯示這個函式的位元組碼。
attributes 不同值的陣列,表示這個類的附加資訊,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 註解。

可以用 javap 檢視編譯後的 Java Class 檔案位元組碼。

如果你編譯下面這個簡單的類:

package org.jvminternals;
public class SimpleClass {
    public void sayHello() {
        System.out.println("Hello");
    }
}

執行下面的命令,就可以得到下面的結果輸出: javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class。

public class org.jvminternals.SimpleClass
  SourceFile: "SimpleClass.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#17         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            //  "Hello"
   #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            //  org/jvminternals/SimpleClass
   #6 = Class              #24            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/jvminternals/SimpleClass;
  #14 = Utf8               sayHello
  #15 = Utf8               SourceFile
  #16 = Utf8               SimpleClass.java
  #17 = NameAndType        #7:#8          //  "<init>":()V
  #18 = Class              #25            //  java/lang/System
  #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;
  #20 = Utf8               Hello
  #21 = Class              #28            //  java/io/PrintStream
  #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V
  #23 = Utf8               org/jvminternals/SimpleClass
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)V
{
  public org.jvminternals.SimpleClass();
    Signature: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
        0: aload_0
        1: invokespecial #1    // Method java/lang/Object."<init>":()V
        4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0      5      0    this   Lorg/jvminternals/SimpleClass;

  public void sayHello();
    Signature: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
        0: getstatic      #2    // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc            #3    // String "Hello"
        5: invokevirtual  #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0      9      0    this   Lorg/jvminternals/SimpleClass;
}

這個 class 檔案展示了三個主要部分:常量池、構造器方法和 sayHello 方法。

  • 常量池:提供了通常由符號表提供的相同資訊,詳細描述見下文。
  • 方法:每一個方法包含四個區域,
    • 簽名和訪問標籤
    • 位元組碼
    • LineNumberTable:為偵錯程式提供原始碼中的每一行對應的位元組碼資訊。上面的例子中,Java 原始碼裡的第 6 行與 sayHello 函式位元組碼序號 0 相關,第 7 行與位元組碼序號 8 相關。
    • LocalVariableTable:列出了所有棧幀中的區域性變數。上面兩個例子中,唯一的區域性變數就是 this。

這個 class 檔案用到下面這些位元組碼操作符:

aload0 這個操作碼是aload格式操作碼中的一個。它們用來把物件引用載入到操作碼棧。 表示正在被訪問的區域性變數陣列的位置,但只能是0、1、2、3 中的一個。還有一些其它類似的操作碼用來載入非物件引用的資料,如iload, lload, float 和 dload。其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double。區域性變數陣列位置大於 3 的區域性變數可以用 iload, lload, float, dload 和 aload 載入。這些操作碼都只需要一個運算元,即陣列中的位置
ldc 這個操作碼用來將常量從執行時常量池壓棧到運算元棧
getstatic 這個操作碼用來把一個靜態變數從執行時常量池的靜態變數列表中壓棧到運算元棧
invokespecial, invokevirtual 這些操作碼屬於一組函式呼叫的操作碼,包括:invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual。在這個 class 檔案中,invokespecial 和 invokevirutal 兩個指令都用到了,兩者的區別是,invokevirutal 指令呼叫一個物件的例項方法,invokespecial 指令呼叫例項初始化方法、私有方法、父類方法。
return 這個操作碼屬於ireturn、lreturn、freturn、dreturn、areturn 和 return 操作碼組。每個操作碼返回一種型別的返回值,其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double,a 表示 物件引用。沒有字首型別字母的 return 表示返回 void

跟任何典型的位元組碼一樣,運算元與區域性變數、運算元棧、執行時常量池的主要互動如下所示。

構造器函式包含兩個指令。首先,this 變數被壓棧到運算元棧,然後父類的構造器函式被呼叫,而這個構造器會消費 this,之後 this 被彈出運算元棧。

JVM內幕:Java虛擬機器詳解

sayHello() 方法更加複雜,正如之前解釋的那樣,因為它需要用執行時常量池中的指向符號引用的真實引用。第一個操作碼 getstatic 從System類中將out靜態變數壓到運算元棧。下一個操作碼 ldc 把字串 “Hello” 壓棧到運算元棧。最後 invokevirtual 操作符會呼叫 System.out 變數的 println 方法,從運算元棧作彈出”Hello” 變數作為 println 的一個引數,並在當前執行緒開闢一個新棧幀。

JVM內幕:Java虛擬機器詳解

類載入器

JVM 啟動時會用 Bootstrap 類載入器載入一個初始化類,然後這個類會在public static void main(String[])呼叫之前完成連結和初始化。執行這個方法會執行載入、連結、初始化需要的額外類和介面。

載入(Loading)是這樣一個過程,找到代表這個類的 class 檔案或根據特定的名字找到介面型別,然後讀取到一個位元組陣列中。接著,這些位元組會被解析檢驗它們是否代表一個 Class 物件幷包含正確的 major、minor 版本資訊。直接父類的類和介面也會被載入進來。這些操作一旦完成,類或者介面物件就從二進位制表示中建立出來了。

連結(Linking)是校驗類或介面並準備型別和父類父介面的過程。連結過程包含三步:校驗(verifying)、準備(preparing)、部分解析(optionally resolving)。

校驗會確認類或者介面表示是否結構正確,以及是否遵循 Java 語言和 JVM 的語義要求,比如會進行下面的檢查:

  1. 格式一致且格式化正確的符號表
  2. final 方法和類沒有被過載
  3. 方法遵循訪問控制關鍵詞
  4. 方法引數的數量、型別正確
  5. 位元組碼沒有不當的操作棧資料
  6. 變數在讀取之前被初始化過
  7. 變數值的型別正確

在驗證階段做這些檢查意味著不需要在執行階段做這些檢查。連結階段的檢查減慢了類載入的速度,但是它避免了執行這些位元組碼時的多次檢查。

準備過程包括為靜態儲存和 JVM 使用的資料結構(比如方法表)分配記憶體空間。靜態變數建立並初始化為預設值,但是初始化程式碼不在這個階段執行,因為這是初始化過程的一部分。

解析是可選的階段。它包括通過載入引用的類和介面來檢查這些符號引用是否正確。如果不是發生在這個階段,符號引用的解析要等到位元組碼指令使用這個引用的時候才會進行。

類或者介面初始化由類或介面初始化方法<clinit>的執行組成。

JVM內幕:Java虛擬機器詳解

JVM 中有多個類載入器,分飾不同的角色。每個類載入器由它的父載入器載入。bootstrap 載入器除外,它是所有最頂層的類載入器。

  • Bootstrap 載入器一般由原生程式碼實現,因為它在 JVM 載入以後的早期階段就被初始化了。bootstrap 載入器負責載入基礎的 Java API,比如包含 rt.jar。它只載入擁有較高信任級別的啟動路徑下找到的類,因此跳過了很多普通類需要做的校驗工作。
  • Extension 載入器載入了標準 Java 擴充套件 API 中的類,比如 security 的擴充套件函式。
  • System 載入器是應用的預設類載入器,比如從 classpath 中載入應用類。
  • 使用者自定義類載入器也可以用來載入應用類。使用自定義的類載入器有很多特殊的原因:執行時重新載入類或者把載入的類分隔為不同的組,典型的用法比如 web 伺服器 Tomcat。
JVM內幕:Java虛擬機器詳解

加速類載入

共享類資料(CDS)是Hotspot JVM 5.0 的時候引入的新特性。在 JVM 安裝過程中,安裝程式會載入一系列核心 JVM 類(比如 rt.jar)到一個共享的記憶體對映區域。CDS 減少了載入這些類需要的時間,提高了 JVM 啟動的速度,允許這些類被不同的 JVM 例項共享,同時也減少了記憶體消耗。

方法區在哪裡

The Java Virtual Machine Specification Java SE 7 Edition 中寫得很清楚:“儘管方法區邏輯上屬於堆的一部分,簡單的實現可以選擇不對它進行回收和壓縮。”。Oracle JVM 的 jconsle 顯示方法區和 code cache 區被當做為非堆記憶體,而 OpenJDK 則顯示 CodeCache 被當做 VM 中物件堆(ObjectHeap)的一個獨立的域。

Classloader 引用

所有的類載入之後都包含一個載入自身的載入器的引用,反過來每個類載入器都包含它們載入的所有類的引用。

執行時常量池

JVM 維護了一個按型別區分的常量池,一個類似於符號表的執行時資料結構。儘管它包含更多資料。Java 位元組碼需要資料。這個資料經常因為太大不能直接儲存在位元組碼中,取而代之的是儲存在常量池中,位元組碼包含這個常量池的引用。執行時常量池被用來上面介紹過的動態連結。

常量池中可以儲存多種型別的資料:

  • 數字型
  • 字串型
  • 類引用型
  • 域引用型
  • 方法引用

示例程式碼如下:

Object foo = new Object();

寫成位元組碼將是下面這樣:

 0:     new #2             // Class java/lang/Object
 1:    dup
 2:    invokespecial #3    // Method java/ lang/Object "&lt;init&gt;"( ) V

new 操作碼的後面緊跟著運算元 #2 。這個運算元是常量池的一個索引,表示它指向常量池的第二個實體。第二個實體是一個類的引用,這個實體反過來引用了另一個在常量池中包含 UTF8 編碼的字串類名的實體(// Class java/lang/Object)。然後,這個符號引用被用來尋找 java.lang.Object 類。new 操作碼建立一個類例項並初始化變數。新類例項的引用則被新增到運算元棧。dup 操作碼建立一個運算元棧頂元素引用的額外拷貝。最後用 invokespecial 來呼叫第 2 行的例項初始化方法。操作碼也包含一個指向常量池的引用。初始化方法把運算元棧出棧的頂部引用當做此方法的一個引數。最後這個新物件只有一個引用,這個物件已經完成了建立及初始化。

如果你編譯下面的類:

package org.jvminternals;
public class SimpleClass {

    public void sayHello() {
        System.out.println("Hello");
    }

}

生成的類檔案常量池將是這個樣子:

Constant pool:
   #1 = Methodref          #6.#17         //  java/lang/Object."&lt;init&gt;":()V
   #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            //  "Hello"
   #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            //  org/jvminternals/SimpleClass
   #6 = Class              #24            //  java/lang/Object
   #7 = Utf8               &lt;init&gt;
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/jvminternals/SimpleClass;
  #14 = Utf8               sayHello
  #15 = Utf8               SourceFile
  #16 = Utf8               SimpleClass.java
  #17 = NameAndType        #7:#8          //  "&lt;init&gt;":()V
  #18 = Class              #25            //  java/lang/System
  #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;
  #20 = Utf8               Hello
  #21 = Class              #28            //  java/io/PrintStream
  #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V
  #23 = Utf8               org/jvminternals/SimpleClass
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)V

這個常量池包含了下面的型別:

Integer 4 位元組常量
Long 8 位元組常量
Float 4 位元組常量
Double 8 位元組常量
String 字串常量指向常量池的另外一個包含真正位元組 Utf8 編碼的實體
Utf8 Utf8 編碼的字元序列位元組流
Class 一個 Class 常量,指向常量池的另一個 Utf8 實體,這個實體包含了符合 JVM 內部格式的類的全名(動態連結過程需要用到)
NameAndType 冒號(:)分隔的一組值,這些值都指向常量池中的其它實體。第一個值(“:”之前的)指向一個 Utf8 字串實體,它是一個方法名或者欄位名。第二個值指向表示型別的 Utf8 實體。對於欄位型別,這個值是類的全名,對於方法型別,這個值是每個引數型別類的類全名的列表。
Fieldref, Methodref, InterfaceMethodref 點號(.)分隔的一組值,每個值都指向常量池中的其它的實體。第一個值(“.”號之前的)指向類實體,第二個值指向 NameAndType 實體。

異常表

異常表像這樣儲存每個異常處理資訊:

  • 起始點(Start point)
  • 結束點(End point)
  • 異常處理程式碼的 PC 偏移量
  • 被捕獲異常的常量池索引

如果一個方法有定義 try-catch 或者 try-finally 異常處理器,那麼就會建立一個異常表。它為每個異常處理器和 finally 程式碼塊儲存必要的資訊,包括處理器覆蓋的程式碼塊區域和處理異常的型別。

當方法丟擲異常時,JVM 會尋找匹配的異常處理器。如果沒有找到,那麼方法會立即結束並彈出當前棧幀,這個異常會被重新拋到呼叫這個方法的方法中(在新的棧幀中)。如果所有的棧幀都被彈出還沒有找到匹配的異常處理器,那麼這個執行緒就會終止。如果這個異常在最後一個非守護程式丟擲(比如這個執行緒是主執行緒),那麼也有會導致 JVM 程式終止。

Finally 異常處理器匹配所有的異常型別,且不管什麼異常丟擲 finally 程式碼塊都會執行。在這種情況下,當沒有異常丟擲時,finally 程式碼塊還是會在方法最後執行。這種靠在程式碼 return 之前跳轉到 finally 程式碼塊來實現。

符號表

除了按型別來分的執行時常量池,Hotspot JVM 在永久代還包含一個符號表。這個符號表是一個雜湊表,儲存了符號指標到符號的對映關係(也就是 Hashtable<Symbol*, Symbol>),它擁有指向所有符號(包括在每個類執行時常量池中的符號)的指標。

引用計數被用來控制一個符號從符號表從移除的過程。比如當一個類被解除安裝時,它擁有的在常量池中所有符號的引用計數將減少。當符號表中的符號引用計數為 0 時,符號表會認為這個符號不再被引用,將從符號表中解除安裝。符號表和後面介紹的字串表都被儲存在一個規範化的結構中,以便提高效率並保證每個例項只出現一次。

字串表

Java 語言規範要求相同的(即包含相同序列的 Unicode 指標序列)字串字面量必須指向相同的 String 例項。除此之外,在一個字串例項上呼叫 String.intern() 方法的返回引用必須與字串是字面量時的一樣。因此,下面的程式碼返回 true:

("j" + "v" + "m").intern() == "jvm"

Hotspot JVM 中 interned 字串儲存在字串表中。字串表是一個雜湊表,儲存著物件指標到符號的對映關係(也就是Hashtable<oop, Symbol>),它被儲存到永久代中。符號表和字串表的實體都以規範的格式儲存,保證每個實體都只出現一次。

當類載入時,字串字面量被編譯器自動 intern 並加入到符號表。除此之外,String 類的例項可以呼叫 String.intern() 顯式地 intern。當呼叫 String.intern() 方法時,如果符號表已經包含了這個字串,那麼就會返回符號表裡的這個引用,如果不是,那麼這個字串就被加入到字串表中同時返回這個引用。

相關文章