JVM 內部原理(一)— 概述

JVM 內部原理(一)— 概述

介紹

版本:Java SE 7html

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

目錄

  • 線程獨享(Threads)web

    • JVM 系統線程(JVM System Threads)
    • 程序計數器(PC)
    • 棧(Stack)編程

      • 本地(方法)棧(Native (Method) Stack)
      • 棧約束(Stack Restrictions)
      • 幀(Frame)
      • 本地變量數組(Local Variable Array)
      • 操做數棧(Operand Stack)
    • 動態連接(Dynamic Linking)bootstrap

  • 線程共享(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、一、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虛擬機

結束

相關文章
相關標籤/搜索