JVM結構的簡單梳理

鵬鵬de部落格發表於2019-06-06

JVM是什麼

JVM是一種用於計算裝置的規範,它是一個虛構出來的計算機,是通過軟體模擬物理機器執行程式的執行器.
JVM遮蔽了與具體作業系統平臺相關的資訊,使Java程式只需生成在JVM上執行的位元組碼,就可以在多種平臺上不加修改地執行.
JVM在執行位元組碼時,實際上最終還是把位元組碼解釋成具體平臺上的機器指令執行.

JVMProcess.png


JVM的基本特性

  • 基於棧(Stack-based)的虛擬機器 : 不同於Intel x86和ARM等比較流行的計算機處理器都是基於暫存器(register)架構,JVM是基於棧執行的.
  • 符號引用(Symbolic reference) : 除基本型別外的所有Java型別(類和介面)都是通過符號引用取得關聯的,而非顯式的基於記憶體地址的引用.
  • 垃圾回收機制 : 類的例項通過使用者程式碼進行顯式建立,但卻通過垃圾回收機制自動銷燬.
  • 通過明確清晰基本型別確保平臺無關性 : 像C/C++等傳統程式語言對於int型別資料在同平臺上會有不同的位元組長度.JVM卻通過明確的定義基本型別的位元組長度來維持程式碼的平臺相容性,從而做到平臺無關.
  • 網路位元組序(Network byte order) : Java class檔案的二進位制表示使用的是基於網路的位元組序(network byte order).為了在使用小端(little endian)的Intel x86平臺和在使用了大端(big endian)的RISC系列平臺之間保持平臺無關,必須要定義一個固定的位元組序.JVM選擇了網路傳輸協議中使用的網路位元組序,即基於大端(big endian)的位元組序.


JVM的流程結構

JVMStructure.png

1. Java編譯(Java Compiler)

Java位元組碼是一種執行於Java和機器語言的中間語言,Java位元組碼也是部署Java程式的最小單元.
JVM本身就是用於執行Java位元組碼的執行器,所以'.java'原始碼檔案要先編譯為'.class'二進位制位元組碼.

JavaCompiler.png

ps. javap -c/-verbose 可以將'.class'已可閱讀方式輸出

生成的'.class'檔案由以下幾部分組成 :

  • 結構資訊 : 包括class檔案格式版本號及各部分的數量與大小的資訊.
  • 後設資料 : 對應於Java原始碼中宣告與常量的資訊.包含類/繼承的超類/實現的介面的宣告資訊、域與方法宣告資訊和常量池.
  • 方法資訊 : 對應Java原始碼中語句和表示式對應的資訊.包含位元組碼、異常處理器表、求值棧與區域性變數區大小、求值棧的型別記錄、除錯符號資訊.

Java位元組碼中有4中表示呼叫方法的操作碼 :

  • invokeinterface: 呼叫介面方法
  • invokespecial: 呼叫初始化方法、私有方法、或父類中定義的方法
  • invokestatic: 呼叫靜態方法
  • invokevirtual: 呼叫例項方法

2. 類載入子系統(Class Loader Subsystem)

注意! 類載入的幾個階段是按順序開始,而不是按順序進行或完成.
因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中呼叫或啟用另一個階段.

2.1 Loading

ClassLoading.png

中文名稱 實現語言 作用
根載入器 C++ 在執行JVM時建立,用於載入JavaAPIs,包括Object類,不是ClassLoader子類
擴充套件載入器 Java 用於載入除基本JavaAPIs以外擴充套件類,也用於載入各種安全擴充套件功能
系統載入器 Java 載入應用程式相關的類與使用者指定的ClassPath裡的類
使用者自定義載入器 Java 應用程式根據自身需要自定義的ClassLoader,如Tomcat、JBoss會根據J2EE規範自行實現ClassLoader
ps.
載入過程中會先檢查類是否被已載入,檢查順序是自底向上(見上圖),
只要某個Classloader已載入就視為已載入此類,保證此類只所有 ClassLoader載入一次.
而載入的順序是自頂向下,也就是由上層來逐層嘗試載入此類(見上圖).

2.2 Linking

ClassLinking.png

2.2.1. 驗證(Verifying) :

驗證類是否符合Java規範和JVM規範(編譯階段的語法語義分析不同).
大部分TCK的測試用例都用於檢測對於給定的錯誤的類檔案是否能得到相應的驗證錯誤資訊.

TCK(Technology Compatibility Kit),由Oracle提供的測試工具.

TCK通過執行大量的測試用例(包括大量通過不同方式生成的錯誤類檔案)來驗證JVM規範.
只有通過TCK測試的JVM才能被稱作是JVM.

類似TCK,還有一個JCP(http://jcp.org),用於驗證新的Java技術規範.

對於一個JCP,必須具有詳細的文件,相關的實現以及提交給JSR的TCK測試.
如果使用者想像JSR一樣使用新的Java技術,那他必須先從RI提供者那裡得到許可,或者自己直接實現它並對之進行TCK測試.

ps. 
一般情況由javac編譯的class檔案是不會有問題的,
但可能有人的class檔案是通過其他方式編譯出來的,
這就有可能不符合JVM的編譯規則,就需要過濾掉這部分不合法檔案

2.2.2. 準備(Preparing) :

根據記憶體需求準備相應的資料結構,並分別描述出類中定義的欄位方法以及實現的介面資訊.
被final修飾的靜態變數,會直接賦值為使用者的定義值.
為類的靜態變數分配記憶體,並設定類變數的初始值為預設值(不初始化靜態程式碼塊).

ps. '記憶體分配'僅包括類的靜態變數,不包括例項變數,例項變數會在物件例項化時隨物件一塊分配在Java堆中

基本資料型別與referece的預設值,如下 :

資料型別 預設零值
int 0
long 0L
short (short)0
char '\u0000'
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

注意!

  1. 就基本資料型別來說,對於類變數(static)和全域性變數,如果不顯式地對其賦值而直接使用,則系統會為其賦予預設的零值,而對於區域性變數來說,在使用前必須顯式地為其賦值,否則編譯時不通過.
  2. 對於同時被static和final修飾的常量,必須在宣告的時候就為其顯式地賦值,否則編譯時不通過,而被final修飾的常量則既可以在宣告時顯式地為其賦值,也可以在類初始化時顯式地為其賦值.總之,在使用前必須為其顯式地賦值,系統不會為其賦予預設零值.
  3. 對於引用資料型別reference來說,如陣列引用、物件引用等,如果沒有對其進行顯式地賦值而直接使用,系統都會為其賦予預設的零值,即null.
  4. 如果在陣列初始化時沒有對陣列中的各元素賦值,那麼其中的元素將根據對應的資料型別而被賦予預設的零值.

2.2.3. 解析(Resolving) :

將常量池中的所有符號引用(字面量描述)轉為直接引用(物件和例項的地址指標、例項變數和方法的偏移量).
可以認為一些靜態繫結的會被解析,動態繫結則只會在執行時進行解析.靜態繫結包括一些final方法(不可以重寫)、static方法(只會屬於當前類)、構造器(不會被重寫).

2.3 Initializing

這是類載入的最後階段.為類的變數初始化合適的值.
如果執行的是靜態變數,那麼就會使用使用者指定的值覆蓋之前在準備階段設定的初始值.
如果執行的是static程式碼塊,那麼在初始化階段,JVM就會執行static程式碼塊中定義的所有操作.

注意!

  1. JVM必須確保一個類在初始化的過程中,如果是多執行緒需要同時初始化它,僅僅只能允許其中一個執行緒對其執行初始化操作,其餘執行緒必須等待,只有在活動執行緒執行完對類的初始化操作之後,才會通知正在等待的其他執行緒.
  2. 非靜態類在例項化類,在Java堆中建立物件的時候,才會進行初始化,
    即在類被Java程式"第一次主動使用"的時候,才會觸發初始化操作(如果還沒有載入,則會順勢觸發類的載入過程).

3. 執行時資料區(Runtime Data Areas)

RuntimeDataAreas.png

3.1 堆(Heap)

  1. JVM所管理的記憶體中最大的一塊,是所有執行緒共享的一塊記憶體區域,在JVM啟動時建立.
  2. Heap是JVM用來儲存物件例項以及陣列值的區域,幾乎所有的物件例項以及陣列都在這裡分配記憶體,Heap中的物件的記憶體需要等待GC進行回收.
  3. 由於Heap共享多個執行緒的記憶體,所儲存的資料不是執行緒安全的.
  4. 若是在Heap中沒有記憶體完成例項分配,並且Heap也無法再擴充套件時,將會丟擲OutOfMemoryError異常.
  5. 注意.Heap空間的大小JVM、是否不執行垃圾回收,包括晉升老年代的年齡閥值等等配置都是可以修改設定的.
  6. 已知Heap是所有執行緒共享的,因此在其上進行物件記憶體的分配是需要進行加鎖,這也導致了new物件的開銷是比較大的.
  7. Oracle Hotspot JVM為了提升物件記憶體分配的效率,對於所建立的執行緒都會分配一塊獨立的空間TLAB(Thread Local Allocation Buffer),

    其大小由JVM根據執行的情況計算而得,在TLAB上分配物件時不需要加鎖,因此JVM在給執行緒的物件分配記憶體時會盡量的在TLAB上分配,

    以上情況下JVM中分配物件記憶體的效能和C基本是一樣高效的,但如果物件過大的話則仍然是直接使用堆空間分配.
  8. TLAB僅作用於新生代的Eden空間,因此在編寫Java程式時,通常多個小的物件比大的物件分配起來更加高效.
3.1.1 GC堆(Garbage Collected Heap)

GCHeap.png

Heap劃分為兩大塊 :

  1. 新生代(Young Generation)
# 其分為以下幾塊區域
# Eden Space : 任何新進入執行時資料區域的例項都會存放在此
# S0 Survivor Space : 存在時間較長,經過垃圾回收沒有被清除的例項,就從Eden搬到了S0
# S1 Survivor Space : 存在時間更長的例項,就從S0搬到了S1

所有新建立的Object都將會儲存在新生代中.晉升到老年代有以下幾種情況 :

  1. 每經歷一次垃圾回收,物件的年齡加1(首次進Survivor區後初始年齡為1),當增加至一定程度(預設為15)時,晉升為老年代.
  2. 當一次Minor GC後,物件不夠Survivor區完全容納,會直接晉升為老年代.
  3. 當新生代的相同年齡的物件超過Survivor區的50%時,年齡大於或等於其相同年齡的物件,會直接晉升為老年代.
  1. 老年代(Old/Tenured Generation)
# Tenured : 主要存放應用程式中生命週期長的記憶體物件

老年代的物件比較穩定,所以Major GC不會頻繁執行.觸發Major GC(Full GC)有以下情況 :

  1. 當有新生代的物件晉升入老年代,導致空間不夠用時才觸發Major GC.
  2. 當無法找到足夠大的連續空間分配給新建立的較大物件時也會提前觸發一次Major GC進行垃圾回收騰出空間.
  3. 發生Minor GC時,JVM會檢測之前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小,
    如果大於,則進行一次Major GC,
    如果小於,則檢視HandlePromotionFailure設定是否允許擔保失敗,
    如果允許,那隻會進行一次Minor GC,
    如果不允許,則改為進行一次Major GC.

Major GC的耗時比較長,需要先掃描再回收,且為了減少記憶體碎片導致的記憶體損耗,一般都需要進行合併整理方便下次直接分配.
老年代也會存在記憶體容量不過的情況,也會丟擲OutOfMemoryError異常.

ps. JDK1.8,方法區(HotSpot的永久代(Permanent Generation)),已替換為元空間(Metaspace),使用的是直接記憶體,受本機可用記憶體的限制,並且永遠不會得到java.lang.OutOfMemoryError(可限制大小).

3.1.2 執行時常量池(Runtime Constant Pool)

ConstantPoolInfo.png

ps. JDK1.7及之後版本的JVM已經將執行時常量池從方法區中移了出來,在Java Heap中開闢了一塊區域存放執行時常量池

Class檔案中有類的版本、欄位、方法、介面等描述資訊外,還有常量池資訊(見上圖,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後存放到常量池中).
當常量池無法再申請到記憶體時是會丟擲OutOfMemoryError異常.

ps. 執行時常量池中的內容主要是從各個型別的class檔案的常量池中獲取

!注意!
執行時常量是相對於常量來說的,它具備一個重要特徵是 : 動態性
值相同的動態常量與我們通常說的常量只是來源不同,但是都是儲存在池內同一塊記憶體區域.
Java並不要求常量一定只能在編譯期產生,執行期間也可能產生新的常量,這些常量被放在執行時常量池中.
這裡所說的常量包括:基本型別包裝類(包裝類不管理浮點型,整形只會管理-128到127)和String類(也可以通過String.intern()方法可以強制將String放入常量池).

常量池是為了避免頻繁的建立和銷燬物件而影響系統效能,其實現了物件的共享.
例如字串常量池,在編譯階段就把所有的字串文字放到一個常量池中.

節省記憶體空間:常量池中所有相同的字串常量被合併,只佔用一個空間.
節省執行時間:比較字串時,==比equals()快.對於兩個引用變數,只用==判斷引用是否相等,也就可以判斷實際值是否相等.

3.2 程式計數器(Program Counter Register)

每個執行緒都會有一個程式計數器,各執行緒之間計數器互不影響,獨立儲存,我們稱這類記憶體區域為"執行緒私有"的記憶體.
程式計數器是一塊較小的記憶體空間.當位元組碼直譯器工作時,通過改變這個計數器的值來選取下一條需要執行的位元組碼指令.

程式計數器主要有兩個作用 :

  1. 位元組碼直譯器通過改變程式計數器來依次讀取指令,從而實現程式碼的流程控制,如:順序執行、分支、選擇、迴圈、跳轉、異常處理、執行緒恢復等基礎功能.
  2. 在多執行緒的情況下,程式計數器用於記錄當前執行緒執行的位置,從而當執行緒被切換回來的時候能夠知道該執行緒上次執行到哪兒了.

ps. 程式計數器是唯一一個不會出現OutOfMemoryError的記憶體區域,它的生命週期隨著執行緒的啟動而建立,隨著執行緒的結束而死亡.
ps. 若下一步執行的指令為Native的話,則PC暫存器中不儲存任何資訊.

3.3 本地方法棧(Native Method Stack)

為非Java編寫的本地代程定義的棧空間.也就是說它基本上是用於通過JNI(Java Native Interface)方式呼叫和執行的C/C++程式碼,根據具體情況,C棧或C++棧將會被建立.
此區域還用於儲存每個native方法呼叫的狀態.

本地方法棧虛擬機器棧所發揮的作用非常相似,區別是:

  • 虛擬機器棧為虛擬機器執行Java方法服務,而本地方法棧則為虛擬機器使用到的Native方法服務.
  • 本地方法被執行的時候,在本地方法棧也會建立一個棧幀,用於存放該本地方法的區域性變數表、運算元棧、動態連結、出口資訊.
  • 方法執行完畢後相應的棧幀也會出棧並釋放記憶體空間,也會出現StackOverFlowError和OutOfMemoryError兩種異常.

ps. 本地方法棧在HotSpot JVM中與虛擬機器棧合二為一

3.4 虛擬機器棧(VM Stack)

VMStack.png

虛擬機器棧是執行緒私有的,不能被任何其他執行緒引用,並跟隨執行緒的啟動而建立.其中儲存的資料無素稱為棧幀(Stack Frame).
虛擬機器棧會擁有多個棧幀(Stack Frame).JVM會把棧楨壓入虛擬機器棧或從中彈出一個棧幀.
如果有任何異常丟擲,如printStackTrace()方法輸出的棧跟蹤資訊的每一行表示一個棧幀.

注意.
1. 如果執行緒中的計算需要比允許的更大的虛擬機器棧,則JVM會丟擲一個StackOverflowError.
2. 如果可以動態擴充套件虛擬機器棧,並且嘗試進行擴充套件但記憶體不足以實現擴充套件,或者可以記憶體不足以為新執行緒建立初始虛擬機器棧,則JVM丟擲一個OutOfMemoryError.
3.4.1 棧幀(Stack Frame)

棧幀用於儲存資料部分結果動態連結返回地址排程異常以及屬於當前執行方法的執行時常量池的引用等資訊.
本地變數陣列和運算元棧的大小在編譯時就已確定,所以屬在執行時屬於方法的棧幀大小是固定的.
由於除了推送和彈出幀之外,永遠不會直接操作虛擬機器棧.虛擬機器棧的記憶體不需要是連續的.

ps.無論是'return語句'還是'丟擲異常',不管哪種返回方式都會導致棧幀被彈出.

3.4.1.1 區域性變數(Local Variables)

每個棧幀包含一個稱為區域性變數的變數陣列,用於存放方法引數和方法內定義的區域性變數.

幀的區域性變數陣列的長度在編譯時確定,並以類或介面的二進位制表示形式提供,同時提供與幀相關的方法的程式碼.
JVM使用區域性變數在方法呼叫上傳遞引數.在類方法呼叫中,任何引數都從區域性變數0開始的連續區域性變數中傳遞.
在例項方法呼叫中,區域性變數0總是用於傳遞對呼叫例項方法的物件的引用.
隨後,任何引數都在從區域性變數1開始的連續區域性變數中傳遞,以及其後的就是真正的方法的本地變數.

區域性變數表的容量以變數槽(Variable Slot)為最小單位 :
一個Slot可以存放一個32位以內(boolean、byte、char、short、int、float、reference和returnAddress)的資料型別,reference型別表示一個物件例項的引用,returnAddress已經很少見了,可以忽略.
對於64位的資料型別(Java語言中明確的64位資料型別只有long和double),JVM會以高位對齊的方式為其分配兩個連續的Slot空間.

3.4.1.2 運算元棧(Operand Stacks)

OperandStacks.png

ps.
在概念模型中,一個活動執行緒中兩個棧幀是相互獨立的.但大多數虛擬機器實現都會做一些優化處理,
即上圖,讓下一個棧幀的部分運算元棧與上一個棧幀的部分區域性變數表重疊在一起,這樣的好處是方法呼叫時可以共享一部分資料,而無須進行額外的引數複製傳遞.

每個棧幀包含一個後進先出(LIFO)堆疊,稱為其運算元棧,幀的運算元棧的最大深度在編譯時已確定,並與用於與幀相關的方法的程式碼一起提供.
當一個方法執行開始時,這個方法的運算元棧是空的,在方法執行過程中,會有各種位元組碼指令往運算元棧中寫入和提取內容,也就是出棧/入棧操作.
JVM提供指令以將區域性變數或欄位中的常量或值載入到運算元棧上.
其他JVM指令從運算元棧中獲取運算元,對它們進行操作,並將結果推回運算元棧.
運算元棧還用於準備要傳遞給方法和接收方法結果的引數.

3.4.1.3 動態連線(Dynamic Linking)

每個棧幀都包含一個指向執行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線.
位元組碼中方法呼叫指令是以常量池中的指向方法的符號引用為引數的,動態連結將這些符號方法引用轉換為具體的方法引用.

ps. 注意!有一部分符號引用會在類載入階段或第一次使用的時候轉化為直接引用,這種轉化稱為靜態解析,另外一部分在每次的執行期間轉化為直接引用,這部分稱為動態連線.

3.4.1.4 返回地址(Return Address)

當一個方法被執行後,有兩種方式退出這個方法:

  1. 正常方法呼叫完成(Normal Method Invocation Completion)

    如果呼叫不會直接從Java虛擬機器或執行顯式throw語句引發異常,則方法呼叫會正常完成.
    如果當前方法的呼叫正常完成,則可以將值返回給呼叫方法.
    當被呼叫的方法執行其中一個返回指令時,就會發生這種情況,返回指令的選擇必須適合於返回值的型別(如果有的話).
    這種退出方法的方式是,執行引擎遇到任意一個方法返回的位元組碼指令.

  1. 突然方法呼叫完成(Abrupt Method Invocation Completion)

    在方法執行過程中遇到了異常,且這個異常沒有在方法體內得到處理(即本方法異常處理表中沒有匹配的異常處理器),則方法呼叫會突然完成,就導致方法退出.
    這種退出方式不會給上層呼叫者產生任何返回值.

無論採用何種退出方式,在方法退出後,都需要返回到方法被呼叫的位置,程式才能繼續執行,
方法返回時可能需要在棧幀中儲存一些資訊,用來幫助恢復它的上層方法的執行狀態.
一般來說,
方法正常退出時,呼叫者的程式計數器的值可以作為返回地址,棧幀中很可能會儲存這個計數器值.
而方法異常退出 時,返回地址是通過異常處理器表來確定的,棧幀中一般不會儲存這部分資訊.

ps.
方法退出的過程實際上等同於把當前棧幀出棧,因此退出時可能執行的操作有 : 
恢復上層方法的區域性變數表和運算元棧,把返回值(如果有的話)壓入呼叫者棧幀的運算元棧中,調整程式計數器的值以指向方法呼叫指令後面的一條指令等.

4. 元空間(Metaspace)

Metaspace.png

ps. JDK 1.8之後,方法區(Oracle Hotspot JVM)的永久代被徹底移除了,取而代之是元空間,元空間使用的是直接記憶體

Metaspace的組成 :

  1. Klass Metaspace:
    Klass Metaspace就是用來存klass的,klass是我們熟知的class檔案在JVM裡的執行時資料結構.
    我們看到的類似A.class其實是存在Heap裡的,是java.lang.Class的一個物件例項.
    這塊記憶體是緊接著Heap的,和之前的永久代一樣,這塊記憶體大小可通過-XX:CompressedClassSpaceSize引數來控制,
    這個引數預設是1G,但是這塊記憶體也可以沒有,假如沒有開啟壓縮指標就不會有這塊記憶體,這種情況下klass都會存在NoKlass Metaspace裡,
    另外如果我們把-Xmx設定大於32G的話,其實也是沒有這塊記憶體的,因為會這麼大記憶體會關閉壓縮指標開關.
    還有就是這塊記憶體最多隻會存在一塊.
  2. NoKlass Metaspace:
    NoKlass Metaspace專門來存klass相關的其他的內容,比如Method,ConstantPool等.
    這塊記憶體是由多塊記憶體組合起來的,所以可以認為是不連續的記憶體塊組成的.
    這塊記憶體是必須的,雖然叫做NoKlass Metaspace,但是也其實可以存klass的內容,在第一點已經提到了.
ps. 
Klass Metaspace和NoKlass Mestaspace都是所有classloader共享的,所以類載入器們要分配記憶體,
但是每個類載入器都有一個SpaceManager,來管理屬於這個類載入的記憶體小塊.
如果Klass Metaspace用完了,那就會OutOfMemoryError,不過一般情況下不會,
NoKlass Mestaspace是由一塊塊記憶體慢慢組合起來的,在沒有達到限制條件的情況下,會不斷加長這條鏈,讓它可以持續工作.

Metaspace的特點 :

  1. 類及相關的後設資料的生命週期與類載入器的一致.
  2. 類的後設資料放入Native Memory,字串池和類的靜態變數放入Java Heap中.
  3. 每個載入器有專門的儲存空間.
  4. 只進行線性分配.
  5. 不會單獨回收某個類.
  6. 省掉了GC掃描及壓縮的時間(永久代會為GC帶來不必要的複雜度,並且回收效率偏低).
  7. 元空間裡的物件的位置是固定的.
  8. 如果GC發現某個類載入器不再存活了,會把相關的空間整個回收掉.

5. 執行引擎(Execution Engine)

JVM通過類載入器把位元組碼載入執行時資料區,由執行引擎執行.
執行引擎以指令為單位讀入Java位元組碼,就像CPU一個接一個的執行機器命令一樣.
每個位元組碼命令包含一位元組的操作碼和可選的運算元.執行引擎讀取一個指令並執行相應的運算元,然後去讀取並執行下一條指令.

5.1 直譯器(Interpreter)

讀取解釋逐一執行每一條位元組碼指令.
因為直譯器逐一解釋和執行指令,直譯器解釋位元組碼的速度更快,但對解釋結果的執行速度較慢.所有的解釋性語言都有類似的缺點.
直譯器的缺點是,當多次呼叫一種方法時,每次都需要新的解釋.

5.2 即時編譯器(JIT(Just-In-Time) Compiler)

JITCompiler.png

即時編譯器的引入用來彌補直譯器的不足.執行引擎先以直譯器的方式執行,然後在合適的時機,即時編譯器把整修位元組碼編譯成原生程式碼.

首先,即時編譯器先把位元組碼通過中間程式碼生成器(Itermediate Representation Generator)轉為一種中間形式的表示式.
然後,程式碼優化器(Code Optimizer)負責優化上面生成的程式碼.
最後,目的碼生成器(Target Code Generator)負責生成機器程式碼或原生程式碼.
期間,Profiler會負責查詢熱點程式碼,即該方法是否被多次呼叫.

ps.
Oracle Hotspot VM使用的即時編譯器稱為Hotspot編譯器.
之所以稱為Hotspot是因為Hotspot Compiler會根據分析找到具有更高編譯優先順序的熱點程式碼,然後所這些熱點程式碼轉為原生程式碼.並且通過對原生程式碼的快取,編譯後的程式碼能具有更快的執行速度.
如果一個被編譯過的方法不再被頻繁呼叫,也即不再是熱點程式碼,Hotspot VM會把這些原生程式碼從快取中刪除並對其再次使用直譯器模式執行.
Hotspot VM有Server VM和Client VM之後,它們所使用的即時編譯器也有所不同.

5.3 GC收集器(Garbage Collector)

GC是後臺的守護程式.
有多種且針對不同區域的GC收集器(如.Serial收集器、CMS(Concurrent Mark Sweep)收集器、G1(GarbageFirst)收集器等等).

相關文章