深入淺出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
操作數棧
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對更多細節,請閱讀後續內容。
動態鏈接
線程之間共享
堆
- 青年代
- 通常又被分割為Eden跟Survivor兩個部分
- 通常又被分割為Eden跟Survivor兩個部分
- 老年代(也稱之為終身代)
- 永久代
內存管理
對象和數組永遠都不會被顯式釋放,因此隻能依靠垃圾回收器來自動地回收它們。
通常,以如下的步驟進行:- 新對象和數組被創建在年輕代
- 次垃圾回收器將在年輕代上執行。那些仍然存活著的對象,將被從eden區移動到survivor區
- 主垃圾回收器將會把對象在代與代之間進行移動,主垃圾回收器通常會導致應用程序的線程暫停。那些仍然存活著的對象將被從年輕代移動到老年代
- 永久代會在每次老年代被回收的時候同時進行,它們在兩者中其一滿了之後都會被回收
非堆式內存
- 永久代中包含:
- 方法區
- 內部字符串
- 方法區
- 代碼緩存:用於編譯以及存儲方法,這些方法已經被JIT編譯成本地代碼
JIT編譯
方法區
- 類加載器的引用
- 運行時常量池
- 數值常量
- 字段引用
- 方法引用
- 屬性
- 數值常量
- 字段數據
- 對每個字段
- 名稱
- 類型
- 修改器
- 屬性
- 對每個字段
- 方法數據
- 對每個方法
- 名稱
- 返回值
- 參數類型(按順序)
- 修改器
- 屬性
- 對每個方法
- 方法體
- 對每個方法
- 字節碼
- 操作數棧大小
- 局部變量大小
- 局部變量表
- 異常表
- 對每個異常處理器
- 起始點
- 終止點
- 對處理器代碼的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 |
package org.jvminternals; public class SimpleClass { public void sayHello() { System.out.println("Hello"); } }
如果你運行下麵的命令,那麼你將看到下麵的輸出:
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 |


類加載器
加載: 加載是這樣一個過程:查找表示該類或接口類型的類文件,並把它讀到一個字節數組中。接著,這些字節會被解析以確認它們是否表示一個Class對象以及是否有正確的主、次版本號。任何被當做直接superclass的類或接口也一同被加載。一旦這些工作完成,一個類或接口對象將會從二進製表示中創建。
鏈接: 鏈接包含了對該類或接口的驗證,準備類型以及該類的直接父類跟父接口。簡而言之,鏈接包含三個步驟:驗證、準備以及解析(optional)
驗證: 該階段會確認類以及接口的表示形式在結構上的正確性,同時滿足Java編程語言以及JVM語義上的要求。比如,接下來的檢查動作將會被執行:
- 一致以及正確被格式化的符號表
- final方法或類沒有被override
- 方法必須帶有訪問控製關鍵字
- 方法有正確的參數值跟類型
- 字節碼不會對棧進行不正確得篡改
- 變量在被讀取之前已經進行了初始化
- 變量具有正確的類型
在驗證階段執行這些檢查意味著在運行時可以免去在鏈接階段進行這些動作,雖然拖慢了類的加載速度,然而它避免了在執行字節碼的時候執行這些檢查。
準備: 包含了對靜態存儲的內存分配以及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偏移量
- 被捕獲的異常類的常量池索引
符號表
內部字符串(字符串表)
"j" + "v" + "m").intern() == "jvm"
在JVM中,內部字符串被存儲在字符串表中。字符串表是一個hashtable映射對象指針到符號(比如:Hashtable<oop,Symbol>),它被存儲在永久代裏。
最後更新:2017-11-26 22:04:45