閱讀312 返回首頁    go 小米 go 小米6


深入淺出JVM

原文地址:https://blog.jamesdbloom.com/JVMInternals.html(轉載請注明英文原文出處以及本文地址)

這篇文章簡要解析了JVM的內部結構。下麵這幅圖展示了一個典型的JVM(符合JVM Specification Java SE 7 Edition)所具備的關鍵內部組件。


上圖展示的所有這些組件都將在下麵兩個章節中被解析。第一章包含將會在每個線程上創建的組件;第二章包含那些不依賴於線程即可創建的組件(線程間可共享的組件)。

  • 線程內創建
    • JVM係統線程
    • 單個線程
    • 程序計數器(PC)
    • 原生棧
    • 棧的限製
    • Frame
    • 局部變量數組
    • 操作數棧
    • 動態鏈接
  • 線程之間共享
    • 內存管理
    • 非堆內存區
    • JIT編譯
    • 方法區
    • 類文件結構
    • 類加載器
    • 更快的類加載
    • 方法區位置
    • 類加載器引用
    • 運行時常量池
    • 異常表
    • 符號表
    • (內部字符串)字符串表

線程內組件

一個線程是對程序的一次執行。JVM允許一個應用創建多個線程並行地執行。在HotspotJVM中,對Java線程和原生操作係統線程之間有一個直接的映射。在所有用於創建Java線程的狀態(諸如:thread-local存儲、分配緩衝區、同步對象、棧以及程序計數器)準備好之後,原生線程才會被創建。而一旦Java線程終止,原生線程將會被回收。因此,操作係統將會調度所有線程並分配給它們任何可用的CPU時間片。一旦原生線程被實例化完成,它將調用Java線程上的run()方法。當run()方法返回,未捕獲的異常被處理,原生線程會確認JVM是否需要隨著線程的終止而終止(比如它是最後一個非deamon線程)。一旦線程被終止,無論是原生線程還是Java線程,它們所占用的資源都會被釋放。

JVM係統線程

如果你用jconsole或者任何其他的debug工具查看,可能會看到有許多線程在後台運行。這些運行著的後台線程不包含主線程,主線程是基於執行publicstatic void main(String[]) 的需要而被創建的。而這些後台線程都是被主線程所創建。在HotspotJVM中主要的後台係統線程,見下表:

VM 線程 該線程用於等待執行一係列能夠使得JVM到達一個“safe-point”的操作。
而這些操作不得不發生在一個獨立的線程上的原因是:它們都要求JVM處於一個——無法修改堆的safepoint。
被這個線程執行的該類操作都是“stop-the-world”型的垃圾回收、線程棧回收、線程擱置以及有偏差的鎖定撤銷。
周期性的任務線程 該線程用於響應timer事件(例如,中斷),這些事件用於調度執行周期性的操作
GC 線程 這些線程支持在JVM中不同類型的垃圾回收
編譯器線程 它們用於在運行時將字節碼編譯為本地機器碼
信號分發線程 該線程接收發送給JVM的信號,並通過調用JVM合適的方法進行處理

單個線程

每個線程的一次執行都包含如下的組件

程序計數器(PC)

除非當前指令或者操作碼是原生的,否則當前指令或操作碼的地址都需要依賴於PC來尋址。如果當前方法是原生的,那麼該PC即為undefined。所有的CPU都有一個PC,通常PC在每個指令執行後被增加以指向即將執行的下一條指令的地址。JVM使用PC來跟蹤正在執行的指令的位置。事實上,PC被用來指向methodarea的一個內存地址。

每個線程都有屬於它自己的棧,用於存儲在線程上執行的每個方法的frame。棧是一個後進先出的數據結構,這可以使得當前正在執行的方法位於棧的頂部。對於每個方法的執行,都會有一個新的frame被創建並被入棧到棧的頂部。當方法正常的返回或在方法執行的過程中遇到未捕獲的異常時frame會被出棧。棧不會被直接進行操作,除了push/ pop frame 對象。因此可以看出,frame對象可能會被分配在堆上,並且內存也沒必要是連續的地址空間(請注意區分frame的指針跟frame對象)。

原生棧

不是所有的JVM都支持原生方法,但那些支持該特性的JVM通常會對每個線程創建一個原生方法棧。如果對JVM的JNI(JavaNative Invocation)采用c鏈接模型的實現,那麼原生棧也將是一個C實現的棧。在這個例子中,原生棧中參數的順序 、返回值都將跟通常的C程序相同。一個原生方法通常會對JVM產生一個回調(這依賴於JVM的實現)並執行一個Java方法。這樣一個原生到Java的調用發生在棧上(通常在Java棧),與此同時線程也將離開原生棧,通常在Java棧上創建一個新的frame。

棧的限製

一個棧可以是動態的或者是有合適大小的。如果一個線程要求更大的棧,那麼將拋出StackOverflowError異常;如果一個線程要求新創建一個frame,又沒有足夠的內存空間來分配,將會拋出OutOfMemoryError異常。

Frame

對於每一個方法的執行,一個新frame會被創建並被入棧到棧頂。當方法正常返回或在方法執行的過程中遇到未捕獲的異常,frame會被出棧。

每個frame都包含如下部分:

  • 局部變量數組
  • 返回值
  • 操作數棧
  • 對當前方法所屬的類的常量池的引用

局部變量數組

局部變量數組包含了在方法執行期間所用到的所有的變量。包含一個對this的引用,所有的方法參數,以及其他局部定義的變量。對於類方法(比如靜態方法),方法參數的存儲索引從0開始;而對於實例方法,索引為0的槽都為存儲this指針而保留。

一個局部變量,可以是如下類型:

  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress
除了long以及double(它們都占據兩個連續的槽,因為它們有雙倍的寬度為64位,而不是32位)其他所有的類型都在局部變量數組中占據一個獨立的槽位。

操作數棧

操作數棧在字節碼指令被執行的過程中使用。它跟原生CPU使用的通用目的的寄存器類似。大部分的字節碼都把時間花費在跟操作數棧打交道上,通過入棧、出棧、複製、交換或者執行那些生產/消費值的操作。對字節碼而言,那些在局部變量數組和操作數棧之間移動值的指令是非常頻繁的。例如,一個簡單的變量初始化,就導致產生兩個與操作數棧交互的字節碼。
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
對更多細節,請閱讀後續內容。

動態鏈接

每個frame都包含一個對運行時常量池的引用。該引用指向將要被執行的方法所屬的類的常量池。該引用也用於輔助動態鏈接。
C/C++ 代碼通常被編譯為一個對象文件,然後多個對象文件被鏈接到一起從而產生一個可用的artifact,例如一個可執行文件或dll。在鏈接階段,每個對象文件內的符號引用被替代為一個跟最終可執行文件相關的實際內存地址。而對於Java而言,這個鏈接階段將會在運行時被動態的完成。
當一個Java類被編譯時,所有對存儲在類的常量池中的變量以及方法的引用都被當做符號引用。一個符號引用僅僅隻是一個邏輯引用而不是最終指向物理內存地址的引用。JVM的實現可以選擇解析符號引用的時機,該時機可以發生在當類文件被驗證後、被加載後,這稱之eager或靜態分析;不同的是它也可以發生在當符號引用被首次使用的時候,稱之為lazy或延遲分析。但JVM必須保證:解析發生在每個引用被首次使用前,同時在該時間點,如果遇到分析錯誤能夠拋出異常。綁定是一個處理過程,它將被符號引用標識的字段、方法或類替換為一個直接引用。這個處理過程隻發生一次,因為符號引用需要被完全替換。如果一個符號引用關聯著一個類,而該類還沒有被解析,那麼該類也會被立即加載。每個直接引用都被以偏移的方式存儲,該存儲結構關聯著變量或方法的運行時位置。

線程之間共享

堆用來在運行時存儲類的實例和數組。數組和對象永遠不能被分配到棧上,因為frame被設計為在其創建後不可更改大小。Frame隻存儲用於指向堆中的數組和對象的指針。不像原始類型以及存儲在局部變量數組中的引用(以上這些都指存儲在每個frame裏的),對象總是存儲在堆上,所以當一個方法執行結束,它們不會被立即移除(對象隻能被垃圾回收器回收)。
為了支持垃圾回收,堆被分隔為三個部分:
  • 青年代
    • 通常又被分割為Eden跟Survivor兩個部分
  • 老年代(也稱之為終身代)
  • 永久代

內存管理

對象和數組永遠都不會被顯式釋放,因此隻能依靠垃圾回收器來自動地回收它們。

通常,以如下的步驟進行:
  1. 新對象和數組被創建在年輕代
  2. 次垃圾回收器將在年輕代上執行。那些仍然存活著的對象,將被從eden區移動到survivor區
  3. 主垃圾回收器將會把對象在代與代之間進行移動,主垃圾回收器通常會導致應用程序的線程暫停。那些仍然存活著的對象將被從年輕代移動到老年代
  4. 永久代會在每次老年代被回收的時候同時進行,它們在兩者中其一滿了之後都會被回收

非堆式內存

有些對象並不會創建在堆中,這些對象在邏輯上被認為是JVM機製的一部分。
非堆式的內存包括:
  • 永久代中包含:
    • 方法區
    • 內部字符串
  • 代碼緩存:用於編譯以及存儲方法,這些方法已經被JIT編譯成本地代碼

JIT編譯

Java 字節碼是被解釋過的,但它還是沒有在JVM所宿主的CPU上執行原生代碼快。為了提高性能,OracleHotspot VM會尋找那些有規律地執行的字節碼,並把他們編譯為本地原生代碼。而原生代碼將會被存儲在代碼緩存的非“堆”內存區。這樣,HotspotVM會嚐試去選擇最合適的方式在它編譯代碼以及它執行被解釋過代碼的額外時間之間作出權衡。

方法區

方法區存儲了每個類的信息,例如:
  • 類加載器的引用
  • 運行時常量池
    • 數值常量
    • 字段引用
    • 方法引用
    • 屬性
  • 字段數據
    • 對每個字段
      • 名稱
      • 類型
      • 修改器
      • 屬性
  • 方法數據
    • 對每個方法
      • 名稱
      • 返回值
      • 參數類型(按順序)
      • 修改器
      • 屬性
  • 方法體
    • 對每個方法
      • 字節碼
      • 操作數棧大小
      • 局部變量大小
      • 局部變量表
      • 異常表
        • 對每個異常處理器
          • 起始點
          • 終止點
          • 對處理器代碼的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 為該類提供完全限定名在常量池中的索引,例如:org/jamesdbloom/foo/Bar
super_class 提供對其父類的符號引用在常量池中的索引,例如:java/lang/Object
interface 常量池中的數組索引,該數組提供對所有被實現的接口的符號引用
fields 常量池中的數組索引,該數組提供對每個字段的完整描述
methods 常量池中的數組索引,該數組提供對每個方法簽名的完整描述,如果該方法不是抽象的或者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 -constantsclasses/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方法
  • 常量池:它提供了跟通用符號表提供的相同的信息
  • 方法:(每個都包含四個區域)
    • 簽名和訪問標記
    • 字節碼
    • line number表 – 它給調試器提供相關信息,哪一行關聯到哪條字節碼指令。例如,在sayHello方法中,Java代碼的第6行關聯著字節碼指令行數為0,第7行Java代碼關聯著字節碼指令行數為8
    • 局部變量表 – 列出了在frame中提供的所有局部變量,在這個例子中,唯一的局部變量是:this
下麵列出了在該類文件中,使用到的操作碼:
aload_0 該操作碼是形如aload_<n>格式的一組操作碼中其中的一個。
它們都是用來加載一個對象引用到操作數棧。
而“<n>”用於指示要被訪問的對象引用在局部變量數組中的位置,但n的值隻能是0,1,2或3。
也有其他相似的操作碼用來加載非對象引用,如:iload_<n>,lload_<n>,fload_<n>以及dload_<n>
(其中,i表示int,l表示long,f表示float,而d表示double,上麵n的取值範圍對這些*load_<n>同樣適用)。
局部變量的索引如果大於3,可以使用iload,lload,float,dload和aload加載。
這些操作碼都攜帶要被加載的局部變量在數組中的索引。
ldc 該操作碼用來從運行時常量池取出一個常量壓入操作數棧
getstatic 該操作碼用來從運行時常量池的靜態字段列表入棧一個靜態值到操作數棧
invokespecial
invokevirtual
這些操作碼是一組用來執行方法的操作碼
(總共有:invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual這幾種)。
其中,本例中出現的invokevirtual用來執行類的實例方法;
而invokespecial用於執行實例的初始化方法,同時也用於執行私有方法以及屬於超類但被當前類繼承的方法
(超類方法動態綁定到子類)。
return 該操作碼是一組操作碼(ireturn,lreturn,freturn,dreturn,areturn以及return)中的其中一個。
每個操作碼,都是類型相關的返回語句。
其中i代表int,l表示long,f表示float,d表示double而a表示一個對象的引用。
沒有標識符作為首字母的return語句,僅會返回void

就像在其他通用的字節碼中那樣,以上這些操作碼主要用於跟本地變量、操作數棧以及運行時常量池打交道。
構造器有兩個指令,第一個將“this”壓入到操作數棧,接下來該構造器的父構造器被執行,這一操作將導致this被“消費”,因此this將從操作數棧出棧。



而對於sayHello()方法,它的執行將更為複雜。因為它不得不通過運行時常量池,解析符號引用到真實的引用。第一個操作數getstatic,用來入棧一個指向System類的靜態字段out的引用到操作數棧。接下來的操作數ldc,入棧一個字符串字麵量“Hello”到操作數棧。最後,invokevirtual操作數,執行System.out的println方法,這將使得“Hello”作為一個參數從操作數棧出棧,並為當前線程創建一個新的frame。


類加載器

JVM的啟動是通過bootstrap類加載器來加載一個用於初始化的類。在publicstatic void main(String[])被執行前,該類會被鏈接以及實例化。main方法的執行,將順序經曆加載,鏈接,以及對額外必要的類跟接口的初始化。

加載: 加載是這樣一個過程:查找表示該類或接口類型的類文件,並把它讀到一個字節數組中。接著,這些字節會被解析以確認它們是否表示一個Class對象以及是否有正確的主、次版本號。任何被當做直接superclass的類或接口也一同被加載。一旦這些工作完成,一個類或接口對象將會從二進製表示中創建。

鏈接: 鏈接包含了對該類或接口的驗證,準備類型以及該類的直接父類跟父接口。簡而言之,鏈接包含三個步驟:驗證、準備以及解析(optional)

驗證: 該階段會確認類以及接口的表示形式在結構上的正確性,同時滿足Java編程語言以及JVM語義上的要求。比如,接下來的檢查動作將會被執行:

  1. 一致以及正確被格式化的符號表
  2. final方法或類沒有被override
  3. 方法必須帶有訪問控製關鍵字
  4. 方法有正確的參數值跟類型
  5. 字節碼不會對棧進行不正確得篡改
  6. 變量在被讀取之前已經進行了初始化
  7. 變量具有正確的類型
在驗證階段執行這些檢查意味著在運行時可以免去在鏈接階段進行這些動作,雖然拖慢了類的加載速度,然而它避免了在執行字節碼的時候執行這些檢查。
準備: 包含了對靜態存儲的內存分配以及JVM所使用的任何數據結構(比如方法表)。靜態字段都被創建以及實例化為它們的默認值。然而,沒有任何實例化器或代碼在這個階段被執行,因為這些任務將會發生在實例化階段。
解析: 是一個可選的階段。該階段通過加載引用的類或接口來檢查符號引用是否正確。如果在這個點這些檢查沒發生,那麼對符號引用的解析會被推遲到直到它們被字節碼指令使用之前。

實例化 類或接口,包含執行類或接口的實例化方法:<clinit>


在JVM中存在多個不同職責的類加載器。每一個類加載器都代理其已被加載的父加載器(除了bootstrap類加載器,因為它是根加載器)。

Bootstrap類加載器:通常使用原生代碼實現,因為它在JVM啟動後很快就會被初始化。Bootstrap類加載器用於加載最基本的JavaAPI,比如rt.jar。它僅加載那些位於boot類路徑中的信任級別很高的類。因此,它也跳過了很多驗證過程。

Extension 類加載器:從標準的Java擴展API中加載類。例如,安全的擴展功能集。

System 類加載器:這是應用程序默認的類加載器。它從classpath中加載應用程序類。

用戶定義的類加載器:可以額外得定義類加載器來加載應用程序類。用戶定義的類加載器可用於一些特殊的場景,比如:在運行時重新加載類或將一些特殊的類隔離為多個不同的分組(通常web服務器中都會有這樣的需求,比如Tomcat)。

更快的類加載

一個稱之為類數據共享(CDS)的特性自HotspotJVM 5.0開始被引進。在安裝JVM期間,安裝器加載一係列的Java核心類(如rt.jar)到一個經過映射過的內存區進行共享存檔。CDS減少了加載這些類的時間從而提升了JVM的啟動速度,同時允許這些類在不同的JVM實例之間共享。這大大減少了內存碎片。

方法區的位置

JVM Specification Java SE 7 Edition清楚地聲明:盡管方法區是堆的一個邏輯組成部分,但最簡單的實現可能是既不對它進行垃圾回收也不壓縮它。然而矛盾的是利用jconsole查看Oracle的JVM的方法區(以及CodeCache)是非堆形式的。OpenJDK代碼顯示CodeCache相對ObjectHeap而言是VM中一個獨立的域。

類加載器引用

所有的類都包含一個指向加載它們的類加載器的引用。反過來類加載器也包含它加載的所有類的引用。

運行時常量池

JVM對每個類型維護著一個常量池,它是一個跟符號表相似的運行時數據結構,但它包含了更多的數據。Java的字節碼需要一些數據,通常這些數據會因為太大而難以直接存儲在字節碼中。取而代之的一種做法是將其存儲在常量池中,字節碼包含一個對常量池的引用。運行時常量池主要用來進行動態鏈接。

幾種類型的數據會存儲在常量池中,它們是:

  • 數值字麵量
  • 字符串字麵量
  • 類的引用
  • 字段的引用
  • 方法的引用
例如下麵的示例代碼:
Object foo = new Object();

編譯為字節碼將會像如下這樣:
 0: 	new #2 		    // Class java/lang/Object
 1:	dup
 2:	invokespecial #3    // Method java/ lang/Object "<init>"( ) V

“new”操作碼後麵跟著#2操作數。該操作數是一個指向常量池的索引,因此它引用常量池中得第二條記錄。而第二條記錄是一個類的引用,該記錄反過來引用另一個位於常量池裏的記錄(它是一個用UTF8編碼的類名://Class java/lang/Object)。該符號鏈接稍後會用於查找java.lang.Object類。new操作碼創建類的一個實例同時實例化它的變量。這個指向類的新實例的引用會被加入到操作數棧。dup操作碼接著創建一個額外的對操作數棧的棧頂引用的拷貝。同時將this引用加入棧頂。最終,一個實例的初始化方法會被調用(上圖第二行通過調用invokespecial)。this操作數同樣也包含一個對常量池的引用。實例化方法消費棧頂引用(把其視為傳遞給該方法的一個參數)。最終,將會產生一個對新對象的引用(這個引用是既被創建完成也被初始化完成的)。
如果你編譯下麵的這個簡單的類:
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字節的int常量
Long 一個8字節的long常量
Float 一個4字節的float常量
Double 一個8字節的double常量
String 一個String字麵值常量指向常量池中另一個包含最終字節的UTF8記錄
Utf8 一個字節流表示一個Utf8編碼的字串序列
Class 一個Class字麵值常量指向常量池中的另一個Utf8記錄,它包含JVM內部格式的完全限定名
(它用於動態鏈接)
NameAndType 用一個冒號區分一對值,每個值都指向常量池中得其他記錄。
冒號前的第一個值指向一個utf8字符串字麵量表示方法名或者字段名。
第二個值指向一個utf8字符串字麵量表示類型。
舉一個字段的例子是完全限定的類名;
舉一個方法的例子是: 它是一個列表,該列表中每個參數都是完全限定的類名
Fieldref,
Methodref,
InterfaceMethodref
用點來分隔的一對值,每個值指向常量池中的另一個記錄。
點前的第一個值指向一個Class記錄。第二個值指向一個NameAndType記錄

異常表

異常表存儲了每個異常處理器的信息:
  • 起始點
  • 終止點
  • 處理代碼的PC偏移量
  • 被捕獲的異常類的常量池索引
如果一個方法定義了try-catch或try-finally異常處理器,那麼一個異常表將會被創建。它包含了每個異常處理器的信息或者finally塊以及正在被處理的異常類型跟處理器代碼的位置。
當一個異常被拋出,JVM會為當前方法尋找一個匹配的處理器。如果沒有找到,那麼該方法最終會唐突地出棧當前stackframe而異常會被重新拋出到調用鏈(新的frame)。如果在所有的frame都出棧之前還是沒有找到異常處理器,那麼當前線程將會被終止。當然這也可能會導致JVM被終止,如果異常被拋出到最後一個非後台線程的話,比如該線程就是主線程。
最終異常處理器會匹配所有的異常類型並且無論什麼時候該類型的異常被拋出總是會得到執行。在沒有異常拋出的例子中,finally塊仍然會在方法的最後被執行。一旦return語句被執行就會立即跳轉到finally代碼塊繼續執行。

符號表

除了每個類型的運行時常量池,HotspotJVM對每個類型都有一個符號表存儲在“永久代”中。符號表是一個hashtable將符號指針映射到符號(比如:Hashtable<Symbol*,Symbol>),另外符號表還包含一個指針指向所有符號(這囊括了每個類的運行時常量池)。
“引用計數”被用來作為控製某個符號要從符號表裏刪除的機製。例如,當某個類被卸載後,所有它的運行時常量池中的符號的引用計數都會被減一。當符號表中的一個符號的引用計數到達0時,符號表就認為該符號將不會再被引用,而隨後也會被從符號表中卸載。無論是符號表還是字符串表(見下麵),所有的記錄都存儲在一個標準化的表單中以此來提升性能同時可以確認每條記錄僅僅出現一次。

內部字符串(字符串表)

Java語言規範要求相同的字符串字麵量,包含相同的unicode字符序列的字符串字麵量必須關聯到相同的String實例。另外,如果String.intern()在一個字符串實例上被調用,那麼必須返回一個引用,該引用指代的實例必須跟該字符串的字麵量相同。下麵的代碼將返回true。
"j" + "v" + "m").intern() == "jvm"

在JVM中,內部字符串被存儲在字符串表中。字符串表是一個hashtable映射對象指針到符號(比如:Hashtable<oop,Symbol>),它被存儲在永久代裏。
當類被加載時,字符串字麵量會被編譯器自動“內部化”並且被加入到字符表。另外字符串類的實例可以通過調用String.intern()來明確地內部化。當String.intern()被調用,如果符號表裏已經包含該字符串,那麼指向該字符串的引用將被返回。如果該字符串沒有包含在字符表,則會被加入到字符串表同時返回其引用。


原文發布時間為:2014-09-03

本文來自雲棲社區合作夥伴CSDN博客,了解相關信息可以關注CSDN博客。

最後更新:2017-11-26 22:04:45

  上一篇:go  android 中關於 activity 的一些理解
  下一篇:go  如何在Java中選擇Map/List/Set