JVM 內部原理(一)— 概述


JVM 內部原理(一)— 概述

介紹

版本:Java SE 7

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

目錄

  • 線程獨享(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() 字符串表

線程獨享

線程(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)的數據結構,所以當前執行的方法在棧頂部。每次方法調用時,都會創建新的幀並且壓入棧的頂部。當方法正常返回或拋出未捕獲的異常時,幀或從棧頂移除。除了壓入和移除幀對象的操作,棧沒有其他直接的操作,因此幀對象可以分配在堆中,內存並不要求連續。

本地(方法)棧(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 並且對操作數棧進行退棧操作。

sayHello() 方法要更為復雜,因為它必須解析符號引用獲取對運行時常量池的真實引用。第一個操作數 getstatic 用來將對 System 類 out 靜態字段的引用壓入到操作數棧。第二個操作數 ldc 將字符串 “Hello” 壓入到操作數棧頂部。最后一個操作數 invokevirtual 調用 System.out 的 println 方法,對 “Hello” 的操作數進行出棧操作當作參數並且為當前線程創建新的幀。

類裝載器(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 里,有多個不同角色的類裝載器。每個類裝載器到代理裝載它的父裝載器,** 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 需要用到這點)。

快速類加載(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虛擬機

結束


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM