JVM 內部原理(一)— 概述

Richaaaard發表於2016-12-07

JVM 內部原理(一)— 概述

介紹

版本:Java SE 7

圖中顯示元件將會從兩個方面分別解釋。第一部分涵蓋執行緒獨有的元件,第二部分涵蓋獨立於執行緒的元件(即執行緒共享元件)。

JVM 內部原理(一)— 概述

目錄

  • 執行緒獨享(Threads)

    • JVM 系統執行緒(JVM System Threads)
    • 程式計數器(PC)
    • 棧(Stack)

      • 本地(方法)棧(Native (Method) Stack)
      • 棧約束(Stack Restrictions)
      • 幀(Frame)
      • 本地變數陣列(Local Variable Array)
      • 運算元棧(Operand Stack)
    • 動態連結(Dynamic Linking)

  • 執行緒共享(Shared Between Threads)

    • 堆(Heap)
    • 記憶體管理(Memory Management)
    • 非堆記憶體(Non-Heap Memory)
    • JIT 編譯(Just In Time (JIT) Compilation)
    • 方法區(Method Area)
    • 類檔案結構(Class File Structure)
    • 類裝載器(Classloader)
    • 快速類載入(Faster Class Loading)
    • 方法區在哪裡(Where Is The Method Area)
    • 類裝載器引用(Classloader Reference)
    • 執行時常量池(Run Time Constant Pool)
    • 異常表(Exception Table)
    • 識別符號表(Symbol Table)
    • String.intern() 字串表

JVM 內部原理(一)— 概述

執行緒獨享

執行緒(Thread)

執行緒是程式中執行的執行緒。JVM 允許一個應用有多個執行緒併發執行。在 Hotspot JVM 中,一個 Java 執行緒與本地作業系統執行緒(native operating system)有直接對映。在準備好一個 Java 執行緒所需的所有狀態後(比如,執行緒本地儲存-thread-local storage,分配緩衝-allocation buffers,同步物件-synchronization objects,棧-stacks and 程式計數器-the program counter),本地執行緒才建立。一旦 Java 執行緒 中止,本地執行緒立即回收。作業系統負責排程所有執行緒並且將它們分發到可用的 CPU 。一旦系統執行緒初始化成功後,它就會呼叫 Java 執行緒裡的 run() 方法。當 run() 方法返回時,未捕獲的異常會被處理,本地系統執行緒確認 JVM 是否因為執行緒中止而需要被中止(例如,當前執行緒是否為最後一個非控制檯執行緒)。當執行緒中止後,所有為系統執行緒和 Java 執行緒 分配的資源都會被釋放。

JVM 系統執行緒(JVM System Threads)

如果用 jconsole 或其他的 debugger 工具,就會看到有很多執行緒在後臺執行。這些後臺執行緒與主執行緒一同執行,以及作為呼叫 public static void main(String[]) 而建立的主執行緒所建立的任何執行緒。在 Hotspot JVM 中,後臺系統主執行緒有:

VM 執行緒(VM thread) 此執行緒等待操作要求 JVM 所達到的安全點
週期任務執行緒(Periodic task thread) 此執行緒負責定時事件(例如,中斷),用作規劃定期執行的操作
GC 執行緒 這些執行緒支援 JVM 裡各種型別的 GC
編譯器執行緒 這些執行緒在執行時,將位元組碼編譯成本地編碼
訊號分發執行緒 此執行緒接收傳送給 JVM 程式的訊號,並呼叫 JVM 內部合適的方法對訊號進行處理

執行緒獨有

每個執行的執行緒都包括一下元件:

程式計數器(PC)

定址當前指令或操作碼如果當前方法不是 native 的。如果當前方法是 native 的,那麼程式計數器(PC)的值是 undefined 。所有的 CPU 都有程式計數器,通常程式計數器會在執行指令結束後增加,因此它需要保持下一將要執行指令的地址。JVM 用程式計數器來跟蹤指令的執行,程式計數器實際上是會指向方法區(Method Area)的記憶體地址。

棧(Stack)

每個執行緒都有自己的棧(stack),棧內以幀(frame)的形式保持著執行緒內執行的每個方法。棧是一個後進先出(LIFO)的資料結構,所以當前執行的方法在棧頂部。每次方法呼叫時,都會建立新的幀並且壓入棧的頂部。當方法正常返回或丟擲未捕獲的異常時,幀或從棧頂移除。除了壓入和移除幀物件的操作,棧沒有其他直接的操作,因此幀物件可以分配在堆中,記憶體並不要求連續。

JVM 內部原理(一)— 概述

本地(方法)棧(Native (Method) Stack)

並不是所有的 JVM 都支援 native 方法,而那些支援 native 方法的 JVM 都會以執行緒建立 native 方法棧。如果 JVM 使用 C 連結模型(C-linkage model)實現 Java Native Invocation(JNI),那麼 native 棧是一個 C 語言棧。這種情況下,引數和返回值的順序都和 C 程式裡 native 棧的一致。一個 native 方法(取決於 JVM 的實現)通常也可以回撥 JVM 內的 Java 方法。這個從 native 到 Java 的呼叫會發生在 Java 棧中;執行緒會將 native 棧放在一邊,在 Java 棧中建立新的幀。

棧約束(Stack Restrictions)

棧的大小可以是動態的或固定的。如果執行緒請求棧的大小超過了限制,就會丟擲 StackOverflowError 。如果執行緒請求建立新的幀,但此時沒有足夠的記憶體可供分配,就會丟擲 OutOfMemoryError 。

幀(Frame)

每次方法呼叫時,新的幀都會建立並被壓入棧頂。當方法正常返回或丟擲未捕獲異常時,幀會從做退棧操作。詳細的異常處理參加後面 異常表(Exception Table)部分。

每個幀都包括

  • 本地變數陣列(Local variable array)
  • 返回值(Return value)
  • 運算元棧(Operand stack)
  • 當前方法所在類到執行時常量池的引用
本地變數陣列(Local variable array)

本地變數的陣列包括方法執行所需要的所有變數,包括 this 的引用,所有方法引數和其他本地定義的變數。對於那些方法(靜態方法 static method)引數是以零開始的,對於例項方法,零為 this 保留。

本地變數可以是:

  • boolean運算元棧
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

所有的型別都在本地變數陣列中佔一個槽,而 long 和 double 會佔兩個連續的槽,因為它們有雙倍寬度(64-bit 而不是 32-bit)。

對於 64-bit 模型有待進步研究。

運算元棧(Operand stack)

運算元棧在執行位元組碼指令的時候使用,它和通用暫存器在 native CPU 中使用的方式類似。大多數 JVM 位元組碼通過 pushing,popping,duplicating,swapping,或生產消費值的操作使用運算元棧。因此,將值從本地變數陣列和操作棧之間移動的指令通常是位元組碼。例如,一個簡單的變數初始化會生成兩個位元組的編碼與運算元棧互動。


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

本地變數陣列、運算元棧和執行時常量池是如何互動的參見後面 類檔案結構(Class File Structure)部分。

動態連結(Dynamic Linking)

每個幀都有一個對執行時常量池的引用。引用指向幀內正在執行方法所在類使用的常量池。這個引用可以支援動態連結。

C/C++ 編碼通常是首先編譯一個物件檔案,然後多個檔案會被連結到一起生成一個可執行檔案或 dll 檔案。在連結階段,標識引用(symbolic reference)會被真實的記憶體地址所替換,從而關聯到最終的可執行檔案。在 Java 中,這個連結的過程是在執行時動態完成的。

當 Java 類編譯後,所有變數和方法的引用都作為標識引用存於類的常量池中。標識引用只是一個邏輯引用,並非真實實體記憶體的地址指向。JVM 實現廠商可以自行決定何時解析替換標識引用,可以發生在類檔案被驗證及裝載後,這種模式被成為早解析;它也可以發生在第一次使用這個標識引用時,這種模式被成為懶解析或晚解析。但在晚解析模式下,如果解析出錯,JVM 任何時候都需要表現的和第一次解析出錯時一樣。繫結是欄位、方法或類在標識引用被識別後替換成直接引用的過程。它只在標識引用被完全替換後才發生。如果類的標識引用沒有完全被解析,然後這個類被裝載了,每個直接引用都會以偏移量的方式儲存而不是執行時變數或方法的位置。

執行緒共享(Shared Between Threads)

堆(Heap)

堆是執行時分配類例項和陣列記憶體的地方。陣列和物件是不能存在棧裡的,因為棧幀(frame)不是被設計用作此目的,一旦棧幀建立了,它的大小不可更改。幀只用來儲存指向對中物件或陣列的引用。與幀內本地變數陣列裡基本變數和引用不同,物件總是儲存在堆內的,所以在方法結束前,它們不會被移除。而且,物件只能被垃圾回收器移除。

為了支援垃圾回收的機制,堆通常被分為三部分:

  • 新生代(Young Generation)

    • 通常分為 新生者(Eden)和 倖存者(Survivor)
  • 老年代(Old Generation/Tenured Generation)
  • 永久代(Permanent Generation)

記憶體管理(Memory Management)

物件和陣列不會被顯式的移除,而是會被 GC 自動回收。

通常的順序是這樣:

  1. 新的物件和陣列被建立在新生代區
  2. 小的 GC 會發生在新生代,存活的物件會從 新生區(Eden)移到 倖存區(Survivor)
  3. 大的 GC ,通常會導致應用程式執行緒暫停,物件移動會發生在不同代之間。仍然存活的物件會從新生代被移動到老年代。
  4. 永久代的收集時刻都會在對老年代收集時發生。任何一代記憶體使用滿了,會在兩代同時發生收集。

非堆記憶體(Non-Heap Memory)

那些邏輯上被認為是 JVM 機器一部分的物件不會建立與堆上。

非堆記憶體包括:

  • 永久代,包括

    • 方法區
    • interned 字串
  • 編碼快取 用來編譯和儲存那些已經被 JIT 編譯器編譯成 native 碼的方法

JIT 編譯(Just In Time (JIT) Compilation)

Java 位元組碼解釋的速度沒有直接在 JVM 主機 CPU 上執行的 native 碼執行那麼快。為了提升效能,Oracle Hotspot VM 檢視那些定期執行 “熱” 的位元組碼區域,並將它們編譯成 native 碼。native 碼被存在非堆記憶體的編碼快取中。通過這種方式,Hotspot VM 嘗試在額外編譯時間以及執行時額外解釋的時間中做平衡,以獲取更好的效能

方法區(Method Area)

方法區按類存放類相關的資訊:

  • Classloader 引用(Classloader Reference)
  • 執行時常量池(Run Time Constant Pool)
    • 數字常量(Numeric Constants)
    • 欄位引用(Field References)
    • 方法引用(Method Reference)
    • 屬性(Attribute)
  • 欄位資料(Field Data)
    • 按欄位(Per Field)
      • 名稱(Name)
      • 型別(Type)
      • 修飾符(Modifiers)
      • 屬性(Attributes)
  • 方法資料
    • 按方法(Per Method)
      • 名稱(Name)
      • 返回型別(Return Type)
      • 引數型別#有序(Parameter Types in order)
      • 修飾符(Modifiers)
      • 屬性(Attributes)
  • 方法程式碼
    • 按方法(Per Method)
      • 位元組碼(Bytecodes)
      • 運算元棧大小(Operand Stack Size)
      • 本地變數大小(Local Variable Size)
      • 本地變數表(Local Variable Table)
      • 異常表(Exception Table)
        • 按異常來處理(Per Exception Handling)
          • 開始點(Start Point)
          • 終結點(End Point)
          • 處理程式碼的程式計數器偏移(PC Offset for Handler Code)
          • 被捕獲的異常類的常量池的索引(Constant Pool Index for Exception Class Being Caught)

所有的執行緒都共享相同的方法區,所以在訪問方法區資料和處理動態連結時必須保證執行緒安全。如果兩個執行緒同時嘗試訪問一個未載入但只載入一次的欄位或方法,兩個執行緒都必須等到完全載入後才能繼續執行。

類檔案結構(Class File Structure)

一個編譯好的類檔案包括以下的結構

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 索引到 constant_pool 提供識別符號引用到父類,例如,java/lang/Object
interfaces 索引列表到 constant_pool 提供識別符號引用到所有實現的介面
fields 索引列表到 constant_pool 為每個欄位提供完整的描述
methods 索引列表到 constant_pool 為每個方法簽名提供完整的描述,如果方法不是抽象的或 native 的,也會呈現位元組碼
attributes 不同值列表,提供類的額外資訊,包括註解 RetentionPolicy.CLASS 或 RetentionPolicy.RUNTIME

可以通過 javap 命令檢視被編譯的 Java 類的位元組碼。

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

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;
}

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

  • 常量池 - 提供了字元表相同的資訊
  • 方法 - 每個方法包括四個方面
    • 簽名 和 訪問標誌位(access flags)
    • 位元組碼
    • 行號表(LineNumberTable)- 為 debugger 工具提供資訊,為位元組碼指令儲存行號,例如,第 6 行在 sayHello 方法中的位元組碼是 0 ,第 7 行對應的位元組碼是 8 。
    • 本地變數表 - 列出了幀內所有的本地變數,在兩個示例中,只有一個本地變數就是 this 。

以下的位元組碼運算元會在類檔案中被用到。

aload_0 這個操作碼是一組以 aload_<n> 為格式的操作碼中的一個。它們都會裝載一個物件的引用到運算元棧裡。<n> 指的是本地變數列表的訪問位置,只能通過 0、1、2 或 3 來訪問。也有其他類似的操作碼用來裝載值,但不是用作物件引用的 iload_<n>,lload_<n>,float_<n> 和 dload_<n> 這裡 i 是對應 int,l 對應 long,f 對應 float,d 對應 double。本地變數的索引位置大於 3 的可以分別通過 iload、lload、float、dload 以及 aload 來裝載。這些所有的操作碼都以單個運算元來指定要裝載的本地變數的索引位置。
ldc 這個操作碼用來將常量從執行時常量池壓入到運算元棧中。
getstatic 這個操作碼用來將靜態值從執行時常量池內的一個靜態欄位列表中壓入到運算元棧內。
invokespecial,invokevirtual 這兩個操作碼是一組用來呼叫方法操作碼其中的兩個,它們是 invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual。在這個類檔案中,invokespecial 和 invokevirtual 同時被用到,不同之處在於 invokevirtual 呼叫物件類上的一個方法,而 invokespecial 指令用來呼叫例項初始化的方法、私有方法或者當前類父類中的方法。
return 這個操作碼是一組操作碼中的一個,它們是:ireturn,lreturn,freturn,dreturn,areturn 和 return。每個操作碼都是與型別相關的返回語句。i 對應 int,l 對應 long,f 對應 float,d 對應 double 然後 a 是物件引用。不帶首字母的 return 返回 void。

作為位元組碼,大多運算元以下面這種方式與本地變數、運算元棧和執行時常量池進行互動。

構造器有兩個指令,第一個 this 被壓入運算元棧,另一個是其父類的構造器,它在呼叫時會消費 this 並且對運算元棧進行退棧操作。

JVM 內部原理(一)— 概述

sayHello() 方法要更為複雜,因為它必須解析符號引用獲取對執行時常量池的真實引用。第一個運算元 getstatic 用來將對 System 類 out 靜態欄位的引用壓入到運算元棧。第二個運算元 ldc 將字串 “Hello” 壓入到運算元棧頂部。最後一個運算元 invokevirtual 呼叫 System.out 的 println 方法,對 “Hello” 的運算元進行出棧操作當作引數並且為當前執行緒建立新的幀。

JVM 內部原理(一)— 概述

類裝載器(Classloader)

JVM 開始於使用啟動裝載器(bootstrap classloader)裝載一個初始類。類在 public static void main(String[]) 呼叫前完成連結和初始化。這個方法的執行也會驅動裝載、連結和初始化其他所需的類與介面。

裝載(Loading) 是查詢特定名稱的類或介面型別對應的類檔案並將其讀入位元組陣列的過程。位元組符被解析並確定它們所代表的 Class 物件以及是否具備正確的版本(major and minor)。任何直接父類,不論是類還是介面都會被裝載。一旦這個過程完成後,就會從二進位制的表現形式建立類物件或介面物件。

連結(Linking) 是對類或介面進行驗證並準備它們的型別、直接父類以及直接父介面的過程。連結包括三步:驗證、準備和識別(resolving 可選)。

  • 驗證(Verifying) 是確認類或介面的表現形式的結構是否正確,是否遵守 Java 程式語言及 JVM 語法規範的過程。例如:會進行以下檢查

    1. 一致且格式正確的符號表
    2. final 方法/類沒有沒有被過載
    3. 方法符合訪問控制的關鍵字
    4. 方法引數的數量和型別正確
    5. 位元組碼對棧進行正確的操作
    6. 變數在讀取前已被正確的初始化
    7. 變數的型別正確

    在驗證過程進行這些檢查也就意味著無須在執行時進行檢查。在連結時進行驗證會降低類裝載的速度,但同時也避免了在執行位元組碼時,進行多次驗證。

  • 準備(Preparing) 過程涉及為靜態儲存以及任何 JVM 使用的資料結構(比如,方法表)分配記憶體。靜態欄位用預設值進行建立和初始化,但是,沒有初始方法或編碼在這個階段執行,因為這會發生在初始化階段。

  • 解析(Resolving) 是一個可選階段,它涉及到通過裝載引用類和介面的方式檢查標識引用,並檢查引用是否正確。如果沒有在此處進行解析,那麼標識引用的解析過程可以推遲到位元組碼指令使用之前執行。

** 初始化(Initialization)** 類或介面的過程包括執行類或介面初始化方法 <clinit> 的過程。

JVM 內部原理(一)— 概述

在 JVM 裡,有多個不同角色的類裝載器。每個類裝載器到代理裝載它的父裝載器,** bootstrap classloader ** 是頂部的裝載器。

Bootstrap Classloader 通常是用原生程式碼實現的(native code)因為它在 JVM 裝載的早期例項化的。bootstrap classloader 的職責是裝載基本的 Java APIs,包括例如 rt.jar 。它只裝載那些 classpath 下具有高可信度的類;這樣它也會省略很多對普通類需要做的校驗。

Extension Classloader 裝載那些從標準 Java 擴充套件的 API 比如,安全擴充套件功能。

System Classloader 預設的應用裝載器,用來從 classpath 裝載應用程式類。

User Defined Classloader 也可以用來裝載應用程式類。使用使用者定義的 classloader 有很多特殊的原因,包括執行時重新裝載類或者區分不同裝載類的組別(通常在 web 伺服器,如 Tomcat 需要用到這點)。

JVM 內部原理(一)— 概述

快速類載入(Faster Class Loading)

在 Hotspot JVM 5.0 之後引入了一個被稱為 類資料共享(Class Data Sharing-CDS)的新特性。在安裝 JVM 的過程中,JVM 安裝並載入一組 JVM 類的關鍵集合到記憶體對映的共享檔案中,如 rt.jar 。CDS 減少了 JVM 啟動所需的時間,它使得這些類可以在多個不同 JVM 例項共享,從而減少了 JVM 的記憶體佔用。

方法區在哪裡(Where Is The Method Area)

The Java Virtual Machine Specification Java SE 7 Edition 中,明確指出:“儘管方法區(Method Area)邏輯上是堆的一部分,簡單的實現通常既不會對其進行垃圾回收,也不會對其進行壓縮”。相反,Oracle JVM 的 jconsole 顯示方法區(以及程式碼快取)處於非堆中。OpenJDK 的程式碼顯示程式碼快取(CodeCache)在 VM 裡是獨立於物件堆的區域。

類裝載器引用(Classloader Reference)

所有被裝載的類都保留對它裝載器(classloader)的一個引用。反正,裝載器(classloader)也保留了它裝載的所有類的引用。

執行時常量池(Run Time Constant Pool)

JVM 按型別維護常量池和執行時的資料結構,它與標識表類似,只是包含更多的資料。Java 裡的位元組碼需要請求資料,通常這些資料很大,無法直接儲存於位元組碼內,所以它們會被儲存在常量池中,位元組碼裡只保留一個對常量池的引用。執行時的常量池是作動態連結的。

在常量池記憶體儲著幾種型別的資料:

  • 數字(numeric literals)
  • 字串(string literals)
  • 類引用(class references)
  • 欄位引用(field references)
  • 方法引用(method references)

例如以下程式碼:

Object foo = new Object();

用位元組碼錶示會寫成:

0:  new #2          // Class java/lang/Object
1:  dup
2:  invokespecial #3    // Method java/ lang/Object "<init>"( ) V       

new 這個運算元碼(operand code)緊接著 #2 這個運算元。這個操作碼是常量池內的一個索引,因此引用到常量池內的另一個記錄,這個記錄是一個類的引用,這個記錄進一步引用到常量池裡以 UTF8 編碼的字串常量 // Class java/lang/Object 。這個標識連結就能用來查詢 java.lang.Object 類。new 運算元碼建立類例項並初始化其變數。然後一個新的類例項被加入到運算元棧內。dup 操作碼拷貝了運算元棧頂部位置的索引,並將其壓入到運算元棧的頂部。最後,例項初始化方法在第 2 行被 invokespecial 呼叫。這個運算元也包含了對常量池的一個引用。初始化方法進行退棧操作,並將引用作為引數傳遞給方法。這樣一個對新物件的引用就建立並初始化完成了。

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

package org.jvminternals;

public class SimpleClass {

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

}

在生成類檔案的常量池會是下面這樣:

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

常量表裡有如下型別:

Integer 4 byte int 常量
Long 8 byte long 常量
Float 4 byte float 常量
Double 8 byte double 常量
String 字串常量指向常量池中另一個 UTF8 包含真實位元組的記錄
Utf8 一個 UTF8 編碼的字串流
Class 一個類常量指向常量池中另一個 UTF8 包含 JVM 格式的完整類名稱的記錄
NameAndType 以分號分隔的數值對,每個都指向常量池中的一條記錄。分號前的數值指向表示方法或型別名稱的 UTF8 字串記錄,分號後的數值指向型別。如果是欄位,那麼對應完整的累名稱,如果是方法,那麼對應一組包含完整類名的引數列表
Fieldref,Methodref,InterfaceMethodref 以點為分隔符的數值對,每個數值指向常量池裡面的一條記錄。點之前的值指向 Class 記錄,點之後的值指向 NameAndType 記錄

異常表(Exception Table)

異常表按異常處理型別儲存資訊:

  • 開始點(Start point)
  • 結束點(End point)
  • 異常處理程式碼程式計數器的偏移量(PC offset for handler code)
  • 捕獲異常類在常量池中的索引

如果一個方法定義了 try-catch 或 try-finally 異常處理,那麼就會建立一個異常表。它包括了每個異常處理或 finally 塊以及異常處理程式碼應用的範圍,包括異常的型別以及異常處理的程式碼。

當丟擲異常時,JVM 會查詢與當前方法匹配的異常處理程式碼,如果沒有找到,方法就會被異常中止,然後對當前棧楨退棧,並在被呼叫的方法內(新的當前楨)重新丟擲異常。如果沒有找到任何異常處理程式,那麼所有的楨都會被退棧,執行緒被中止。這也可能導致 JVM 本身被中止,如果異常發生在最後一個非後臺執行緒時就會出現這種狀況。例如,如果執行緒是主執行緒。

finally 異常處理匹配所有型別的異常,所以只要有異常丟擲就會執行異常處理。當沒有異常丟擲時,finally 塊仍然會被執行,這可以通過在 return 語句執行之前,跳入 finally 處理程式碼來實現。

識別符號表(Symbol Table)

說到按型別儲存的執行時常量池,Hotspot JVM 的識別符號表是儲存在永久代的。識別符號表用一個 Hashtable 在標識指標與標識之間建立對映(例如,Hashtable<Symbol*, Symbol>)以及一個指向所有識別符號的指標,包括那些被儲存的每個類的執行時常量表。

引用計數用來控制識別符號從識別符號表內移除。例如,當一個類被解除安裝時,所有在執行時常量池裡保留的識別符號引用都會做相應的自減。當識別符號表的引用計數變為零時,識別符號表知道識別符號不再被引用,那麼識別符號就會從識別符號表中解除安裝。無論是識別符號表還是字串表,所有記錄都以都以出現的 canonicalized 形式保持以提高效能,並保證每個記錄都只出現一次。

String.intern() 字串表

Java 語言規範(The Java Language Specification)要求相同的字串文字一致,即包含相同順序的 Unicode 碼指標,指標指向相同的字串例項。如果 String.intern() 在某個字串的例項引用被呼叫,那麼它的值需要與相同字串文字引用的返回值相等。即以下語句為真:

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

在 Hotspot JVM intern 的字串是儲存在字串表裡的,它用一個 Hashtable 在物件指標與字元之間建立對映(例如,Hashtable<oop, Symbol>),它被儲存在永久代裡。對於識別符號表和字串表,所有記錄都是以 canonicalized 形式保持以提高效能,並保證每個記錄都只出現一次。

字串 literals 在編譯時自動被 interned 並在裝載類的時候被載入到字元表裡。字串類的例項也可以顯式呼叫 String.intern() 。當 String.intern() 被呼叫後,如果識別符號表已經包含了該字串,那麼將直接返回字串的引用,如果沒有,那麼字串會被加入到字串表中,然後返回字串的引用。

參考

參考來源:

JVM Specification SE 7 - Run-Time Data Areas

2011.01 Java Bytecode Fundamentals

2013.11 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虛擬機器

結束

相關文章