編譯地址: jamesdbloom
譯者: ImportNew.com - 挖坑的張師傅
原文地址: http://www.importnew.com/17770.html
這篇文章解釋了Java 虛擬機(JVM)的內(nèi)部架構(gòu)。下圖顯示了遵守 Java SE 7 規(guī)范的典型的 JVM 核心內(nèi)部組件只怎。
上圖顯示的組件分兩個章節(jié)解釋晕讲。第一章討論針對每個線程創(chuàng)建的組件,第二章節(jié)討論了線程無關(guān)組件。
- 線程
- JVM 系統(tǒng)線程
- 每個線程相關(guān)的
- 程序計數(shù)器
- 棧
- 本地棧
- 棧限制
- 棧幀
- 局部變量數(shù)組
- 操作數(shù)棧
- 動態(tài)鏈接
- 線程共享
- 堆
- 內(nèi)存管理
- 非堆內(nèi)存
- 即時編譯
- 方法區(qū)
- 類文件結(jié)構(gòu)
- 類加載器
- 更快的類加載
- 方法區(qū)在哪里
- 類加載器參考
- 運行時常量池
- 異常表
- 符號表
- Interned 字符串
線程
這里所說的線程指程序執(zhí)行過程中的一個線程實體。JVM 允許一個應(yīng)用并發(fā)執(zhí)行多個線程。Hotspot JVM 中的 Java 線程與原生操作系統(tǒng)線程有直接的映射關(guān)系涝涤。當線程本地存儲、緩沖區(qū)分配岛杀、同步對象阔拳、棧、程序計數(shù)器等準備好以后类嗤,就會創(chuàng)建一個操作系統(tǒng)原生線程糊肠。Java 線程結(jié)束,原生線程隨之被回收遗锣。操作系統(tǒng)負責調(diào)度所有線程货裹,并把它們分配到任何可用的 CPU 上。當原生線程初始化完畢精偿,就會調(diào)用 Java 線程的 run()
方法弧圆。run()
返回時,被處理未捕獲異常笔咽,原生線程將確認由于它的結(jié)束是否要終止 JVM 進程(比如這個線程是最后一個非守護線程)墓阀。當線程結(jié)束時,會釋放原生線程和 Java 線程的所有資源拓轻。
JVM 系統(tǒng)線程
如果使用 jconsole 或者其它調(diào)試器,你會看到很多線程在后臺運行经伙。這些后臺線程與觸發(fā) public static void main(String[]) 函數(shù)的主線程以及主線程創(chuàng)建的其他線程一起運行扶叉。Hotspot JVM 后臺運行的系統(tǒng)線程主要有下面幾個:
線程相關(guān)組件
每個運行的線程都包含下面這些組件:
程序計數(shù)器(PC)
PC 指當前指令(或操作碼)的地址勿锅,本地指令除外。如果當前方法是 native 方法枣氧,那么PC 的值為 undefined溢十。所有的 CPU 都有一個 PC,典型狀態(tài)下达吞,每執(zhí)行一條指令 PC 都會自增张弛,因此 PC 存儲了指向下一條要被執(zhí)行的指令地址。JVM 用 PC 來跟蹤指令執(zhí)行的位置酪劫,PC 將實際上是指向方法區(qū)(Method Area)的一個內(nèi)存地址吞鸭。
棧(Stack)
每個線程擁有自己的棧,棧包含每個方法執(zhí)行的棧幀覆糟。棧是一個后進先出(LIFO)的數(shù)據(jù)結(jié)構(gòu)刻剥,因此當前執(zhí)行的方法在棧的頂部。每次方法調(diào)用時滩字,一個新的棧幀創(chuàng)建并壓棧到棧頂造虏。當方法正常返回或拋出未捕獲的異常時,棧幀就會出棧麦箍。除了棧幀的壓棧和出棧漓藕,棧不能被直接操作。所以可以在堆上分配棧幀,并且不需要連續(xù)內(nèi)存。
Native棧
并非所有的 JVM 實現(xiàn)都支持本地(native)方法鞭铆,那些提供支持的 JVM 一般都會為每個線程創(chuàng)建本地方法棧衔统。如果 JVM 用 C-linkage 模型實現(xiàn) JNI(Java Native Invocation),那么本地棧就是一個 C 的棧唬涧。在這種情況下,本地方法棧的參數(shù)順序、返回值和典型的 C 程序相同划滋。本地方法一般來說可以(依賴 JVM 的實現(xiàn))反過來調(diào)用 JVM 中的 Java 方法。這種 native 方法調(diào)用 Java 會發(fā)生在棧(一般是 Java 棧)上埃篓;線程將離開本地方法棧处坪,并在 Java 棧上開辟一個新的棧幀。
棧的限制
椉茏ǎ可以是動態(tài)分配也可以固定大小同窘。如果線程請求一個超過允許范圍的空間,就會拋出一個StackOverflowError部脚。如果線程需要一個新的棧幀想邦,但是沒有足夠的內(nèi)存可以分配,就會拋出一個 OutOfMemoryError委刘。
棧幀(Frame)
每次方法調(diào)用都會新建一個新的棧幀并把它壓棧到棧頂丧没。當方法正常返回或者調(diào)用過程中拋出未捕獲的異常時鹰椒,棧幀將出棧。更多關(guān)于異常處理的細節(jié)呕童,可以參考下面的異常信息表章節(jié)漆际。
每個棧幀包含:
- 局部變量數(shù)組
- 返回值
- 操作數(shù)棧
- 類當前方法的運行時常量池引用
局部變量數(shù)組
局部變量數(shù)組包含了方法執(zhí)行過程中的所有變量,包括 this 引用夺饲、所有方法參數(shù)奸汇、其他局部變量。對于類方法(也就是靜態(tài)方法)往声,方法參數(shù)從下標 0 開始擂找,對于對象方法,位置0保留為 this烁挟。
有下面這些局部變量:
- boolean
- byte
- char
- long
- short
- int
- float
- double
- reference
- returnAddress
除了 long 和 double 類型以外婴洼,所有的變量類型都占用局部變量數(shù)組的一個位置。long 和 double 需要占用局部變量數(shù)組兩個連續(xù)的位置撼嗓,因為它們是 64 位雙精度柬采,其它類型都是 32 位單精度。
操作數(shù)棧
操作數(shù)棧在執(zhí)行字節(jié)碼指令過程中被用到且警,這種方式類似于原生 CPU 寄存器粉捻。大部分 JVM 字節(jié)碼把時間花費在操作數(shù)棧的操作上:入棧、出棧斑芜、復(fù)制肩刃、交換、產(chǎn)生消費變量的操作杏头。因此盈包,局部變量數(shù)組和操作數(shù)棧之間的交換變量指令操作通過字節(jié)碼頻繁執(zhí)行。比如醇王,一個簡單的變量初始化語句將產(chǎn)生兩條跟操作數(shù)棧交互的字節(jié)碼呢燥。
int i;
被編譯成下面的字節(jié)碼:
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
更多關(guān)于局部變量數(shù)組、操作數(shù)棧和運行時常量池之間交互的詳細信息寓娩,可以在類文件結(jié)構(gòu)部分找到叛氨。
動態(tài)鏈接
每個棧幀都有一個運行時常量池的引用。這個引用指向棧幀當前運行方法所在類的常量池棘伴。通過這個引用支持動態(tài)鏈接(dynamic linking)寞埠。
C/C++ 代碼一般被編譯成對象文件,然后多個對象文件被鏈接到一起產(chǎn)生可執(zhí)行文件或者 dll焊夸。在鏈接階段仁连,每個對象文件的符號引用被替換成了最終執(zhí)行文件的相對偏移內(nèi)存地址。在 Java中阱穗,鏈接階段是運行時動態(tài)完成的怖糊。
當 Java 類文件編譯時帅容,所有變量和方法的引用都被當做符號引用存儲在這個類的常量池中。符號引用是一個邏輯引用伍伤,實際上并不指向物理內(nèi)存地址。JVM 可以選擇符號引用解析的時機遣钳,一種是當類文件加載并校驗通過后扰魂,這種解析方式被稱為饑餓方式。另外一種是符號引用在第一次使用的時候被解析蕴茴,這種解析方式稱為惰性方式劝评。無論如何 ,JVM 必須要在第一次使用符號引用時完成解析并拋出可能發(fā)生的解析錯誤倦淀。綁定是將對象域蒋畜、方法、類的符號引用替換為直接引用的過程撞叽。綁定只會發(fā)生一次姻成。一旦綁定,符號引用會被完全替換愿棋。如果一個類的符號引用還沒有被解析科展,那么就會載入這個類。每個直接引用都被存儲為相對于存儲結(jié)構(gòu)(與運行時變量或方法的位置相關(guān)聯(lián)的)偏移量糠雨。
線程間共享
堆
堆被用來在運行時分配類實例才睹、數(shù)組。不能在棧上存儲數(shù)組和對象甘邀。因為棧幀被設(shè)計為創(chuàng)建以后無法調(diào)整大小琅攘。棧幀只存儲指向堆中對象或數(shù)組的引用。與局部變量數(shù)組(每個棧幀中的)中的原始類型和引用類型不同松邪,對象總是存儲在堆上以便在方法結(jié)束時不會被移除坞琴。對象只能由垃圾回收器移除。
為了支持垃圾回收機制测摔,堆被分為了下面三個區(qū)域:
- 新生代
- 經(jīng)常被分為 Eden 和 Survivor
- 老年代
- 永久代
內(nèi)存管理
對象和數(shù)組永遠不會顯式回收置济,而是由垃圾回收器自動回收。通常锋八,過程是這樣的:
- 新的對象和數(shù)組被創(chuàng)建并放入老年代浙于。
- Minor垃圾回收將發(fā)生在新生代。依舊存活的對象將從 eden 區(qū)移到 survivor 區(qū)挟纱。
- Major垃圾回收一般會導(dǎo)致應(yīng)用進程暫停羞酗,它將在三個區(qū)內(nèi)移動對象。仍然存活的對象將被從新生代移動到老年代紊服。
- 每次進行老年代回收時也會進行永久代回收檀轨。它們之中任何一個變滿時胸竞,都會進行回收。
非堆內(nèi)存
非堆內(nèi)存指的是那些邏輯上屬于 JVM 一部分對象参萄,但實際上不在堆上創(chuàng)建卫枝。
非堆內(nèi)存包括:
- 永久代,包括:
- 方法區(qū)
- 駐留字符串(interned strings)
代碼緩存(Code Cache):用于編譯和存儲那些被 JIT 編譯器編譯成原生代碼的方法讹挎。
即時編譯(JIT)
Java 字節(jié)碼是解釋執(zhí)行的校赤,但是沒有直接在 JVM 宿主執(zhí)行原生代碼快。為了提高性能筒溃,Oracle Hotspot 虛擬機會找到執(zhí)行最頻繁的字節(jié)碼片段并把它們編譯成原生機器碼马篮。編譯出的原生機器碼被存儲在非堆內(nèi)存的代碼緩存中。通過這種方法怜奖,Hotspot 虛擬機將權(quán)衡下面兩種時間消耗:將字節(jié)碼編譯成本地代碼需要的額外時間和解釋執(zhí)行字節(jié)碼消耗更多的時間浑测。
方法區(qū)
方法區(qū)存儲了每個類的信息,比如:
- Classloader 引用
- 運行時常量池
- 數(shù)值型常量
- 字段引用
- 方法引用
- 屬性
- 字段數(shù)據(jù)
- 針對每個字段的信息
- 字段名
- 類型
- 修飾符
- 屬性(Attribute)
- 方法數(shù)據(jù)
- 每個方法
- 方法名
- 返回值類型
- 參數(shù)類型(按順序)
- 修飾符
- 屬性
- 方法代碼
- 每個方法
- 字節(jié)碼
- 操作數(shù)棧大小
- 局部變量大小
- 局部變量表
- 異常表
- 每個異常處理器
- 開始點
- 結(jié)束點
- 異常處理代碼的程序計數(shù)器(PC)偏移量
- 被捕獲的異常類對應(yīng)的常量池下標
所有線程共享同一個方法區(qū)歪玲,因此訪問方法區(qū)數(shù)據(jù)的和動態(tài)鏈接的進程必須線程安全迁央。如果兩個線程試圖訪問一個還未加載的類的字段或方法,必須只加載一次读慎,而且兩個線程必須等它加載完畢才能繼續(xù)執(zhí)行漱贱。
類文件結(jié)構(gòu)
一個編譯后的類文件包含下面的結(jié)構(gòu):
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];
}
可以用 javap 查看編譯后的 java class 文件字節(jié)碼。
如果你編譯下面這個簡單的類:
package org.jvminternals;
public class SimpleClass {
public void sayHello() {
System.out.println("Hello");
}
}
運行下面的命令夭委,就可以得到下面的結(jié)果輸出: 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;
}
這個 class 文件展示了三個主要部分:常量池、構(gòu)造器方法和 sayHello
方法株灸。
- 常量池:提供了通常由符號表提供的相同信息崇摄,詳細描述見下文。
- 方法:每一個方法包含四個區(qū)域慌烧,
- 簽名和訪問標簽
- 字節(jié)碼
- LineNumberTable:為調(diào)試器提供源碼中的每一行對應(yīng)的字節(jié)碼信息逐抑。上面的例子中,Java 源碼里的第 6 行與 sayHello 函數(shù)字節(jié)碼序號 0 相關(guān)屹蚊,第 7 行與字節(jié)碼序號 8 相關(guān)厕氨。
- LocalVariableTable:列出了所有棧幀中的局部變量。上面兩個例子中汹粤,唯一的局部變量就是 this命斧。
這個 class 文件用到下面這些字節(jié)碼操作符:
跟任何典型的字節(jié)碼一樣,操作數(shù)與局部變量嘱兼、操作數(shù)棧国葬、運行時常量池的主要交互如下所示。
構(gòu)造器函數(shù)包含兩個指令。首先汇四,this 變量被壓棧到操作數(shù)棧接奈,然后父類的構(gòu)造器函數(shù)被調(diào)用,而這個構(gòu)造器會消費 this通孽,之后 this 被彈出操作數(shù)棧序宦。
sayHello()
方法更加復(fù)雜,正如之前解釋的那樣利虫,因為它需要用運行時常量池中的指向符號引用的真實引用挨厚。第一個操作碼 getstatic
從System
類中將out
靜態(tài)變量壓到操作數(shù)棧。下一個操作碼ldc
把字符串 “Hello”
壓棧到操作數(shù)棧糠惫。最后 invokevirtual
操作符會調(diào)用 System.out
變量的 println
方法,從操作數(shù)棧作彈出”Hello”
變量作為 println
的一個參數(shù)钉疫,并在當前線程開辟一個新棧幀硼讽。
類加載器
JVM 啟動時會用 bootstrap 類加載器加載一個初始化類,然后這個類會在public static void main(String[])
調(diào)用之前完成鏈接和初始化牲阁。執(zhí)行這個方法會執(zhí)行加載固阁、鏈接、初始化需要的額外類和接口城菊。
加載(Loading)是這樣一個過程备燃,找到代表這個類的 class 文件或根據(jù)特定的名字找到接口類型,然后讀取到一個字節(jié)數(shù)組中凌唬。接著并齐,這些字節(jié)會被解析檢驗它們是否代表一個 Class 對象并包含正確的 major、minor 版本信息客税。直接父類的類和接口也會被加載進來况褪。這些操作一旦完成,類或者接口對象就從二進制表示中創(chuàng)建出來了更耻。
鏈接(Linking)是校驗類或接口并準備類型和父類父接口的過程测垛。鏈接過程包含三步:校驗(verifying)、準備(preparing)秧均、部分解析(optionally resolving)食侮。
校驗會確認類或者接口表示是否結(jié)構(gòu)正確,以及是否遵循 Java 語言和 JVM 的語義要求目胡,比如會進行下面的檢查:
- 格式一致且格式化正確的符號表
-
final
方法和類沒有被重載 - 方法遵循訪問控制關(guān)鍵詞
- 方法參數(shù)的數(shù)量锯七、類型正確
- 字節(jié)碼沒有不當?shù)牟僮鳁?shù)據(jù)
- 變量在讀取之前被初始化過
- 變量值的類型正確
在驗證階段做這些檢查意味著不需要在運行階段做這些檢查。鏈接階段的檢查減慢了類加載的速度讶隐,但是它避免了執(zhí)行這些字節(jié)碼時的多次檢查起胰。
準備過程包括為靜態(tài)存儲和 JVM 使用的數(shù)據(jù)結(jié)構(gòu)(比如方法表)分配內(nèi)存空間。靜態(tài)變量創(chuàng)建并初始化為默認值,但是初始化代碼不在這個階段執(zhí)行效五,因為這是初始化過程的一部分地消。
解析是可選的階段。它包括通過加載引用的類和接口來檢查這些符號引用是否正確畏妖。如果不是發(fā)生在這個階段脉执,符號引用的解析要等到字節(jié)碼指令使用這個引用的時候才會進行。
類或者接口初始化由類或接口初始化方法<clinit>
的執(zhí)行組成戒劫。
JVM 中有多個類加載器半夷,分飾不同的角色。每個類加載器由它的父加載器加載迅细。bootstrap 加載器除外巫橄,它是所有最頂層的類加載器。
- Bootstrap 加載器一般由本地代碼實現(xiàn)茵典,因為它在 JVM 加載以后的早期階段就被初始化了湘换。bootstrap 加載器負責載入基礎(chǔ)的 Java API,比如包含 rt.jar统阿。它只加載擁有較高信任級別的啟動路徑下找到的類彩倚,因此跳過了很多普通類需要做的校驗工作。
- Extension 加載器加載了標準 Java 擴展 API 中的類扶平,比如 security 的擴展函數(shù)帆离。
- System 加載器是應(yīng)用的默認類加載器,比如從 classpath 中加載應(yīng)用類结澄。
- 用戶自定義類加載器也可以用來加載應(yīng)用類哥谷。使用自定義的類加載器有很多特殊的原因:運行時重新加載類或者把加載的類分隔為不同的組,典型的用法比如 web 服務(wù)器 Tomcat概而。
加速類加載
共享類數(shù)據(jù)(CDS)是Hotspot JVM 5.0 的時候引入的新特性呼巷。在 JVM 安裝過程中,安裝進程會加載一系列核心 JVM 類(比如 rt.jar)到一個共享的內(nèi)存映射區(qū)域赎瑰。CDS 減少了加載這些類需要的時間王悍,提高了 JVM 啟動的速度,允許這些類被不同的 JVM 實例共享餐曼,同時也減少了內(nèi)存消耗压储。
方法區(qū)在哪里
The Java Virtual Machine Specification Java SE 7 Edition 中寫得很清楚:“盡管方法區(qū)邏輯上屬于堆的一部分,簡單的實現(xiàn)可以選擇不對它進行回收和壓縮源譬〖铮”。Oracle JVM 的 jconsle 顯示方法區(qū)和 code cache 區(qū)被當做為非堆內(nèi)存踩娘,而 OpenJDK 則顯示 CodeCache 被當做 VM 中對象堆(ObjectHeap)的一個獨立的域刮刑。
Classloader 引用
所有的類加載之后都包含一個加載自身的加載器的引用,反過來每個類加載器都包含它們加載的所有類的引用。
運行時常量池
JVM 維護了一個按類型區(qū)分的常量池雷绢,一個類似于符號表的運行時數(shù)據(jù)結(jié)構(gòu)泛烙。盡管它包含更多數(shù)據(jù)。Java 字節(jié)碼需要數(shù)據(jù)翘紊。這個數(shù)據(jù)經(jīng)常因為太大不能直接存儲在字節(jié)碼中蔽氨,取而代之的是存儲在常量池中,字節(jié)碼包含這個常量池的引用帆疟。運行時常量池被用來上面介紹過的動態(tài)鏈接鹉究。
常量池中可以存儲多種類型的數(shù)據(jù):
- 數(shù)字型
- 字符串型
- 類引用型
- 域引用型
- 方法引用
示例代碼如下:
Object foo = new Object();
寫成字節(jié)碼將是下面這樣:
0: new #2 // Class java/lang/Object
1: dup
2: invokespecial #3 // Method java/ lang/Object "<init>"( ) V
new
操作碼的后面緊跟著操作數(shù) #2
。這個操作數(shù)是常量池的一個索引踪宠,表示它指向常量池的第二個實體自赔。第二個實體是一個類的引用,這個實體反過來引用了另一個在常量池中包含 UTF8 編碼的字符串類名的實體(// Class java/lang/Object
)柳琢。然后匿级,這個符號引用被用來尋找 java.lang.Object
類。new
操作碼創(chuàng)建一個類實例并初始化變量染厅。新類實例的引用則被添加到操作數(shù)棧。dup
操作碼創(chuàng)建一個操作數(shù)棧頂元素引用的額外拷貝津函。最后用 invokespecial
來調(diào)用第 2 行的實例初始化方法肖粮。操作碼也包含一個指向常量池的引用。初始化方法把操作數(shù)棧出棧的頂部引用當做此方法的一個參數(shù)尔苦。最后這個新對象只有一個引用涩馆,這個對象已經(jīng)完成了創(chuàng)建及初始化。
如果你編譯下面的類:
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
這個常量池包含了下面的類型:
異常表
異常表像這樣存儲每個異常處理信息:
- 起始點(Start point)
- 結(jié)束點(End point)
- 異常處理代碼的 PC 偏移量
- 被捕獲異常的常量池索引
如果一個方法有定義 try-catch
或者 try-finally
異常處理器允坚,那么就會創(chuàng)建一個異常表魂那。它為每個異常處理器和 finally 代碼塊存儲必要的信息,包括處理器覆蓋的代碼塊區(qū)域和處理異常的類型稠项。
當方法拋出異常時涯雅,JVM 會尋找匹配的異常處理器。如果沒有找到展运,那么方法會立即結(jié)束并彈出當前棧幀活逆,這個異常會被重新拋到調(diào)用這個方法的方法中(在新的棧幀中)。如果所有的棧幀都被彈出還沒有找到匹配的異常處理器拗胜,那么這個線程就會終止蔗候。如果這個異常在最后一個非守護進程拋出(比如這個線程是主線程),那么也有會導(dǎo)致 JVM 進程終止埂软。
Finally 異常處理器匹配所有的異常類型锈遥,且不管什么異常拋出 finally 代碼塊都會執(zhí)行。在這種情況下,當沒有異常拋出時所灸,finally 代碼塊還是會在方法最后執(zhí)行丽惶。這種靠在代碼 return 之前跳轉(zhuǎn)到 finally 代碼塊來實現(xiàn)。
符號表
除了按類型來分的運行時常量池庆寺,Hotspot JVM 在永久代還包含一個符號表蚊夫。這個符號表是一個哈希表,保存了符號指針到符號的映射關(guān)系(也就是 Hashtable<Symbol*, Symbol>
)懦尝,它擁有指向所有符號(包括在每個類運行時常量池中的符號)的指針知纷。
引用計數(shù)被用來控制一個符號從符號表從移除的過程。比如當一個類被卸載時陵霉,它擁有的在常量池中所有符號的引用計數(shù)將減少琅轧。當符號表中的符號引用計數(shù)為 0 時,符號表會認為這個符號不再被引用踊挠,將從符號表中卸載乍桂。符號表和后面介紹的字符串表都被保存在一個規(guī)范化的結(jié)構(gòu)中,以便提高效率并保證每個實例只出現(xiàn)一次效床。
字符串表
Java 語言規(guī)范要求相同的(即包含相同序列的 Unicode 指針序列)字符串字面量必須指向相同的 String 實例睹酌。除此之外,在一個字符串實例上調(diào)用 String.intern()
方法的返回引用必須與字符串是字面量時的一樣剩檀。因此憋沿,下面的代碼返回 true:
("j" + "v" + "m").intern() == "jvm"
Hotspot JVM 中 interned 字符串保存在字符串表中。字符串表是一個哈希表沪猴,保存著對象指針到符號的映射關(guān)系(也就是Hashtable<oop, Symbol>
)辐啄,它被保存到永久代中。符號表和字符串表的實體都以規(guī)范的格式保存运嗜,保證每個實體都只出現(xiàn)一次壶辜。
當類加載時,字符串字面量被編譯器自動 intern 并加入到符號表担租。除此之外砸民,String
類的實例可以調(diào)用 String.intern()
顯式地 intern
。當調(diào)用 String.intern()
方法時翩活,如果符號表已經(jīng)包含了這個字符串阱洪,那么就會返回符號表里的這個引用,如果不是菠镇,那么這個字符串就被加入到字符串表中同時返回這個引用冗荸。