這篇文章解釋了Java 虛擬機(JVM)的內部架構。下圖顯示了遵守 Java SE 7 規範的典型的 JVM 核心內部組件。 上圖顯示的組件分兩個章節解釋。第一章討論針對每個線程創建的組件,第二章節討論了線程無關組件。 線程 JVM 系統線程 每個線程相關的 程式計數器 棧 本地棧 棧限制 棧幀 局
這篇文章解釋了Java 虛擬機(JVM)的內部架構。下圖顯示了遵守 Java SE 7 規範的典型的 JVM 核心內部組件。
上圖顯示的組件分兩個章節解釋。第一章討論針對每個線程創建的組件,第二章節討論了線程無關組件。
- 線程
- 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 位元組碼把時間花費在操作數棧的操作上:入棧、出棧、複製、交換、產生消費變數的操作。因此,局部變數數組和操作數棧之間的交換變數指令操作通過位元組碼頻繁執行。比如,一個簡單的變數初始化語句將產生兩條跟操作數棧交互的位元組碼。
1 |
int i;
|
被編譯成下麵的位元組碼:
1 2 |
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
- 老年代
- 永久代
記憶體管理
對象和數組永遠不會顯式回收,而是由垃圾回收器自動回收。通常,過程是這樣的:
- 新的對象和數組被創建並放入老年代。
- Minor垃圾回收將發生在新生代。依舊存活的對象將從 eden 區移到 survivor 區。
- Major垃圾回收一般會導致應用進程暫停,它將在三個區內移動對象。仍然存活的對象將被從新生代移動到老年代。
- 每次進行老年代回收時也會進行永久代回收。它們之中任何一個變滿時,都會進行回收。
非堆記憶體
非堆記憶體指的是那些邏輯上屬於 JVM 一部分對象,但實際上不在堆上創建。
非堆記憶體包括:
- 永久代,包括:
- 方法區
- 駐留字元串(interned strings)
- 代碼緩存(Code Cache):用於編譯和存儲那些被 JIT 編譯器編譯成原生代碼的方法。
即時編譯(JIT)
Java 位元組碼是解釋執行的,但是沒有直接在 JVM 宿主執行原生代碼快。為了提高性能,Oracle Hotspot 虛擬機會找到執行最頻繁的位元組碼片段並把它們編譯成原生機器碼。編譯出的原生機器碼被存儲在非堆記憶體的代碼緩存中。通過這種方法,Hotspot 虛擬機將權衡下麵兩種時間消耗:將位元組碼編譯成本地代碼需要的額外時間和解釋執行位元組碼消耗更多的時間。
方法區
方法區存儲了每個類的信息,比如:
- Classloader 引用
- 運行時常量池
- 數值型常量
- 欄位引用
- 方法引用
- 屬性
- 欄位數據
- 針對每個欄位的信息
- 欄位名
- 類型
- 修飾符
- 屬性(Attribute)
- 針對每個欄位的信息
- 方法數據
- 每個方法
- 方法名
- 返回值類型
- 參數類型(按順序)
- 修飾符
- 屬性
- 每個方法
- 方法代碼
- 每個方法
- 位元組碼
- 操作數棧大小
- 局部變數大小
- 局部變數表
- 異常表
- 每個異常處理器
- 開始點
- 結束點
- 異常處理代碼的程式計數器(PC)偏移量
- 被捕獲的異常類對應的常量池下標
- 每個方法
所有線程共用同一個方法區,因此訪問方法區數據的和動態鏈接的進程必須線程安全。如果兩個線程試圖訪問一個還未載入的類的欄位或方法,必須只載入一次,而且兩個線程必須等它載入完畢才能繼續執行。
類文件結構
一個編譯後的類文件包含下麵的結構:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 文件位元組碼。
如果你編譯下麵這個簡單的類:
1 2 3 4 5 6 |
package org.jvminternals;
public class SimpleClass {
public void sayHello() {
System.out.println( "Hello" );
}
}
|
運行下麵的命令,就可以得到下麵的結果輸出: javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
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 被彈出操作數棧。
sayHello() 方法更加複雜,正如之前解釋的那樣,因為它需要用運行時常量池中的指向符號引用的真實引用。第一個操作碼 getstatic 從System類中將out靜態變數壓到操作數棧。下一個操作碼 ldc 把字元串 “Hello” 壓棧到操作數棧。最後 invokevirtual 操作符會調用 System.out 變數的 println 方法,從操作數棧作彈出”Hello” 變數作為 println 的一個參數,併在當前線程開闢一個新棧幀。
類載入器
JVM 啟動時會用 bootstrap 類載入器載入一個初始化類,然後這個類會在public static void main(String[])調用之前完成鏈接和初始化。執行這個方法會執行載入、鏈接、初始化需要的額外類和介面。
載入(Loading)是這樣一個過程,找到代表這個類的 class 文件或根據特定的名字找到介面類型,然後讀取到一個位元組數組中。接著,這些位元組會被解析檢驗它們是否代表一個 Class 對象並包含正確的 major、minor 版本信息。直接父類的類和介面也會被載入進來。這些操作一旦完成,類或者介面對象就從二進位表示中創建出來了。
鏈接(Linking)是校驗類或介面並準備類型和父類父介面的過程。鏈接過程包含三步:校驗(verifying)、準備(preparing)、部分解析(optionally resolving)。
校驗會確認類或者介面表示是否結構正確,以及是否遵循 Java 語言和 JVM 的語義要求,比如會進行下麵的檢查:
- 格式一致且格式化正確的符號表
- final 方法和類沒有被重載
- 方法遵循訪問控制關鍵詞
- 方法參數的數量、類型正確
- 位元組碼沒有不當的操作棧數據
- 變數在讀取之前被初始化過
- 變數值的類型正確
在驗證階段做這些檢查意味著不需要在運行階段做這些檢查。鏈接階段的檢查減慢了類載入的速度,但是它避免了執行這些位元組碼時的多次檢查。
準備過程包括為靜態存儲和 JVM 使用的數據結構(比如方法表)分配記憶體空間。靜態變數創建並初始化為預設值,但是初始化代碼不在這個階段執行,因為這是初始化過程的一部分。
解析是可選的階段。它包括通過載入引用的類和介面來檢查這些符號引用是否正確。如果不是發生在這個階段,符號引用的解析要等到位元組碼指令使用這個引用的時候才會進行。
類或者介面初始化由類或介面初始化方法<clinit>
的執行組成。
JVM 中有多個類載入器,分飾不同的角色。每個類載入器由它的父載入器載入。bootstrap 載入器除外,它是所有最頂層的類載入器。
- Bootstrap 載入器一般由本地代碼實現,因為它在 JVM 載入以後的早期階段就被初始化了。bootstrap 載入器負責載入基礎的 Java API,比如包含 rt.jar。它只載入擁有較高信任級別的啟動路徑下找到的類,因此跳過了很多普通類需要做的校驗工作。
- Extension 載入器載入了標準 Java 擴展 API 中的類,比如 security 的擴展函數。
- System 載入器是應用的預設類載入器,比如從 classpath 中載入應用類。
- 用戶自定義類載入器也可以用來載入應用類。使用自定義的類載入器有很多特殊的原因:運行時重新載入類或者把載入的類分隔為不同的組,典型的用法比如 web 伺服器 Tomcat。
加速類載入
共用類數據(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 位元組碼需要數據。這個數據經常因為太大不能直接存儲在位元組碼中,取而代之的是存儲在常量池中,位元組碼包含這個常量池的引用。運行時常量池被用來上面介紹過的動態鏈接。
常量池中可以存儲多種類型的數據:
- 數字型
- 字元串型
- 類引用型
- 域引用型
- 方法引用
示例代碼如下:
1 |
Object foo = new Object();
|
寫成位元組碼將是下麵這樣:
1 2 3 |
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 操作碼創建一個操作數棧頂元素引用的額外拷貝。最後用 invokespecial 來調用第 2 行的實例初始化方法。操作碼也包含一個指向常量池的引用。初始化方法把操作數棧出棧的頂部引用當做此方法的一個參數。最後這個新對象只有一個引用,這個對象已經完成了創建及初始化。
如果你編譯下麵的類:
1 2 3 4 5 6 7 8 |
package org.jvminternals;
public class SimpleClass {
public void sayHello() {
System.out.println( "Hello" );
}
}
|
生成的類文件常量池將是這個樣子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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 位元組常量 |
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 會尋找匹配的異常處理器。如果沒有找到,那麼方法會立即結束並彈出當前棧幀,這個異常會被重新拋到調用這個方法的方法中(在新的棧幀中)。如果所有的棧幀都被彈出還沒有找到匹配的異常處理器,那麼這個線程就會終止。如果這個異常在最後