---云開方見日,潮盡爐峰出。揭開JVM的神秘面紗,探尋底層實現(xiàn)原理
?Java Vitural Machine
Java 的源代碼是怎么被機器識別并執(zhí)行的呢?答案是Java虛擬機,即 Java Vitural Machine, 簡稱 JVM.
字節(jié)碼
字節(jié)碼是一種中間狀態(tài)(中間碼)的二進制代碼(文件),Java中所有指令有200個左右,一個字節(jié)(8位)可以存儲256種不同指令信息,一個這樣的字節(jié)稱為字節(jié)碼(ByteCode)。在代碼的執(zhí)行過程中,JVM將字節(jié)碼解釋執(zhí)行,屏蔽對底層操作系統(tǒng)的依賴,JVM 也可以將字節(jié)碼編譯執(zhí)行,如果是熱點代碼,會通過JIT 動態(tài)地編譯為機器碼,提高執(zhí)行效率。
程序的執(zhí)行方式分為靜態(tài)編譯執(zhí)行、動態(tài)編譯執(zhí)行和動態(tài)解釋執(zhí)行。 注意:此處所說的編譯指的是編譯成可讓操作系統(tǒng)直接執(zhí)行的機器碼。
?java執(zhí)行
字節(jié)碼和機器碼的區(qū)別
機器碼是電腦CPU直接讀取運行的機器指令,運行速度最快,但是非常晦澀難懂,也比較難編寫,一般從 業(yè)人員接觸不到。
字節(jié)碼是一種中間狀態(tài)(中間碼)的二進制代碼(文件)。需要直譯器轉(zhuǎn)譯后才能成為機器碼。
加載或存儲指令
在某個棧幀中,通過指令操作數(shù)據(jù)在虛擬機的局部變量表與操作棧之間來回傳輸,常見指令如下
(1)將局部變量表的變量加載到操作棧中:如ILOAD(將int類型的局部變量壓入操作棧)和ALOAD(將對象引用的局部變量壓入棧)等。
(2)從操作棧頂將變量存儲到局部變量表中:如ISTORE、ASTORE等
(3)將常量加載到操作棧頂,這是極為高頻使用的指令。如ICONST、BIPUSH、SIPUSH、LDC等。
ICONST 加載的是 -1 ~ 5 的數(shù)(ICONST 與BIPUSH 的加載界限)。
BIPUSH ,即Byte Immediate PUSH ,加載 -128 ~ 127 之間的數(shù)。
SIPUSH ,即Short Immediate PUSH ,加載 -32768 ~ 32767 之間的數(shù)。
LDC ,即Load Constant ,在 -2147483648 ~ 2147483647 或者是字符串時,JVM采用LDC 指令壓入枝中。
運算指令
對兩個操作棧幀上的值進行運算,并把結(jié)果寫入操作棧,如IADD、IMUL等
類型轉(zhuǎn)換指令
顯示轉(zhuǎn)換兩種不同的數(shù)值類型。如I2L、D2F等
對象創(chuàng)建與訪問指令
根據(jù)對象的創(chuàng)建、初始化、方法調(diào)用相關(guān)指令,常見指令如下:
(1)創(chuàng)建指令:如NEW、NEWAPPAY等。
(2)訪問屬性指令:如GETF!ELD 、PUTFIELD 、GETSTATIC 等。
(3)核查實例類型指令:如INSTANCEOF、CHECKCAST 等。
操作棧管理指令
JVM提供了直接操控操作棧的指令,常見指令如下:
(1)出棧操作:如POP即一個元素,POP2即兩個元素。
(2)復(fù)制棧頂元素并壓入棧:DUP。
方法調(diào)用與返回指令
( I ) INVOKEYIRTUAL 指令:調(diào)用對象的實例方法。
( 2) INVOKESPECIAL 指令:調(diào)用實例初始化方法、私有方法、父類方法等。
( 3 ) INVOKESTATIC 指令: 調(diào)用類靜態(tài)方法。
( 4 ) RETURN 指令: 返回VOID 類型。
同步指令
JVM使用方法結(jié)構(gòu)中的ACC_SYNCHRONIZED標志同步方法,指令集中有MONITORENTER和MONITOREXIT支持synchronized語義。
方法調(diào)用計數(shù)器觸發(fā)即時編譯簡單流程
?方法調(diào)用計數(shù)器觸發(fā)即時編譯
回邊計數(shù)器 它的作用就是統(tǒng)計一個方法中循環(huán)體代碼執(zhí)行的次數(shù),在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令稱為“回邊”。
JIT優(yōu)化
HotSpot 虛擬機使用了很多種優(yōu)化技術(shù),這里只簡單介紹其中的幾種,完整的優(yōu)化技術(shù)介紹可以參考官網(wǎng)內(nèi)容。
(1)公共子表達式的消除
公共子表達式消除是一個普遍應(yīng)用于各種編譯器的經(jīng)典優(yōu)化技術(shù),他的含義是:如果一個表達式E已經(jīng) 計算過了,并且從先前的計算到現(xiàn)在E中所有變量的值都沒有發(fā)生變化,那么E的這次出現(xiàn)就成為了公共 子表達式。對于這種表達式,沒有必要花時間再對他進行計算,只需要直接用前面計算過的表達式結(jié)果 代替E就可以了。
舉個簡單的例子來說明他的優(yōu)化過程,假設(shè)存在如下代碼:
int d = (c*b)*12+a+(a+b*c);
如果這段代碼交給Javac編譯器則不會進行任何優(yōu)化,那生成的代碼如下所示,是完全遵照Java源碼的寫 法直譯而成的。
當這段代碼進入到虛擬機即時編譯器后,他將進行如下優(yōu)化:編譯器檢測到”cb“ ”bc“是一樣的表達 式,而且在計算期間b與c的值是不變的。因此,這條表達式就可能被視為:
int d = E*12+a+(a+E);
這時,編譯器還可能(取決于哪種虛擬機的編譯器以及具體的上下文而定)進行另外一種優(yōu)化:代數(shù)化 簡(Algebraic Simplification),把表達式變?yōu)?
int d = E*13+a*2;
(2)方法內(nèi)聯(lián)
在使用JIT進行即時編譯時,將方法調(diào)用直接使用方法體中的代碼進行替換,這就是方法內(nèi)聯(lián),減少了方 法調(diào)用過程中壓棧與入棧的開銷。同時為之后的一些優(yōu)化手段提供條件。如果JVM監(jiān)測到一些小方法被 頻繁的執(zhí)行,它會把方法的調(diào)用替換成方法體本身。
比如如下代碼
private int add4(int x1, int x2, int x3, int x4) {
return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
return x1 + x2;
}
運行一段時間后優(yōu)化為
private int add4(int x1, int x2, int x3, int x4) {
return x1 + x2 + x3 + x4;
}
(3)逃逸分析
逃逸分析(Escape Analysis)是目前Java虛擬機中比較前沿的優(yōu)化技術(shù)。這是一種可以有效減少Java 程序 中同步負載和內(nèi)存堆分配壓力的跨函數(shù)全局數(shù)據(jù)流分析算法。通過逃逸分析,Java Hotspot編譯器能夠 分析出一個新的對象的引用的使用范圍從而決定是否要將這個對象分配到堆上。
逃逸分析的基本行為就是分析對象動態(tài)作用域:當一個對象在方法中被定義后,它可能被外部方法所引
用,例如作為調(diào)用參數(shù)傳遞到其他地方中,稱為方法逃逸。
類的加載過程
?類的加載過程
在加載的過程中,JVM主要做3件事情
1.通過一個類的全限定名來獲取定義此類的二進制字節(jié)流(class文件) 。在程序運行過程中,當要訪問一個類時,若發(fā)現(xiàn)這個類尚未被加載,并滿足類初始化的條件時,就根據(jù) 要被初始化的這個類的全限定名找到該類的二進制字節(jié)流,開始加載過程
2.將這個字節(jié)流的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
3.在內(nèi)存中創(chuàng)建一個該類的java.lang.Class對象,作為方法區(qū)該類的各種數(shù)據(jù)的訪問入口
雙親委派模型
當一個類加載器收到類加載任務(wù),會先交給其父類加載器去完成,因此最終加載任務(wù)都會傳遞到頂
層的啟動類加載器,只有當父類加載器無法完成加載任務(wù)時,才會嘗試執(zhí)行加載任務(wù)。
采用雙親委派的一個好處是:
比如加載位于rt.jar包中的類java.lang.Object,不管是哪個加載器加載這個類,最終都是委托給頂 層的啟動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個Object 對象。
?雙親委派模型
內(nèi)存布局
?經(jīng)典JVM內(nèi)存布局
程序計數(shù)器
程序計數(shù)器(Program Counter Register),也叫PC寄存器,是一塊較小的內(nèi)存空間,它可以看作是 當前線程所執(zhí)行的字節(jié)碼指令的行號指示器。字節(jié)碼解釋器的工作就是通過改變這個計數(shù)器的值來選取 下一條需要執(zhí)行的字節(jié)碼指令。分支,循環(huán),跳轉(zhuǎn),異常處理,線程回復(fù)等都需要依賴這個計數(shù)器來完 成。
JVM Stack(Java虛擬機棧)
棧(Stack)是一個先進后出的數(shù)據(jù)結(jié)構(gòu),就像子彈的彈夾,最后壓入的子彈先發(fā)射,壓在底部的子彈最后發(fā)射,撞針只能訪問位于頂部的那一顆子彈。每個Java方法在執(zhí)行的時候都會創(chuàng)建一個棧幀 (Stack Frame)相當于子彈。
棧幀(Stack Frame)是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)。棧幀存儲了方法的局部變 量表、操作數(shù)棧、動態(tài)連接和方法返回地址等信息。每一個方法從調(diào)用至執(zhí)行完成的過程,都對應(yīng)著一 個棧幀在虛擬機棧里從入棧到出棧的過程。
?虛擬機棧里從入棧到出棧的過程
現(xiàn)在我們來分析下每個棧幀的內(nèi)部結(jié)構(gòu),包括局部變量表、操作棧、動態(tài)鏈接、方法返回地址等。
(1)局部變量表(Local Variable Table)
局部變量表是存放方法參數(shù)和局部變量的區(qū)域。相對于類屬性變量的準備階段和初始化階段來說,局部變量表沒有準備階段,必須顯示初始化。如果是非靜態(tài)方法,則在index[0]的位置上存儲的是方法所屬對象的引用實例,隨后存儲的是參數(shù)和局部變量。前面提到的字節(jié)碼STORE指令就是將操作棧中的計算完成的局部變量寫回局部變量表的存儲空間內(nèi)。
存儲容量:局部變量表的容量以變量槽(Variable Slot)為最小單位,Java虛擬機規(guī)范并沒有定義一個槽所應(yīng)該占用 內(nèi)存空間的大小,但是規(guī)定了一個槽應(yīng)該可以存放一個32位以內(nèi)的數(shù)據(jù)類型。
(2)操作棧
操作棧是一個初始狀態(tài)為空的桶式結(jié)構(gòu)棧,它是一個后入先出棧(LIFO)。在方法執(zhí)行過程中,會有各種指令往棧中寫入和提取信息。JVM的執(zhí)行引擎是基于棧的執(zhí)行引擎,其中的棧就是指的操作棧。隨著方法執(zhí)行和字節(jié)碼指令的執(zhí)行,會從局部變量表 或?qū)ο髮嵗淖侄沃袕?fù)制常量或變量寫入到操作數(shù)棧,再隨著計算的進行將棧中元素出棧到局部變量表 或者返回給方法調(diào)用者,也就是出棧/入棧操作。一個完整的方法執(zhí)行期間往往包含多個這樣出棧/入棧 的過程。
(3)動態(tài)鏈接
在一個class文件中,一個方法要調(diào)用其他方法,需要將這些方法的符號引用轉(zhuǎn)化為其在內(nèi)存地址中的直接引用,而符號引用存在于方法區(qū)中的運行時常量池。 Java虛擬機棧中,每個棧幀都包含一個指向運行時常量池中該棧所屬方法的符號引用,持有這個引用的目的是為了支持方法調(diào)用過程中的動態(tài)連接(Dynamic Linking)。 這些符號引用一部分會在類加載階段或者第一次使用時就直接轉(zhuǎn)化為直接引用,這類轉(zhuǎn)化稱為靜態(tài)解析。另一部分將在每次運行期間轉(zhuǎn)化為直接引用,這類轉(zhuǎn)化稱為動態(tài)連接。
(4)方法返回地址
方法執(zhí)行時有兩種退出情況,第一, 正常退出,即正常執(zhí)行到任何方法的返回字節(jié)碼指令, 如阻RETURN 、IRETURN 、ARETURN 等;第二, 異常退出。無論何種退出情況,都將返回至方法當前被調(diào)用的位置。方法退出的過程相當于彈出當前棧幀,退出可能有三種方式:
返回值壓入上層調(diào)用棧幀。
異常信息拋給能夠處理的棧幀。
PC 計數(shù)器指向方法調(diào)用后的下一條指令。
本地方法棧
本地方法棧和虛擬機棧相似,區(qū)別就是虛擬機棧為虛擬機執(zhí)行Java服務(wù)(字節(jié)碼服務(wù)),而本地方法棧 為虛擬機使用到的Native方法(比如C++方法)服務(wù)。
Java 堆(重要)
Java堆被所有線程共享,在Java虛擬機啟動時創(chuàng)建。是虛擬機管理最大的一塊內(nèi)存。
Java堆是垃圾回收的主要區(qū)域,主要采用分代回收算法。堆進一步劃分主要是為了更好的回收內(nèi)存或更快的分配內(nèi)存。
堆主要分成兩大塊:新生代和老年代。新生代包含(Eden空間[伊甸園]、From Survivor空間、To Survivor空間)。默認的,Eden : from : to = 8 : 1 : 1 。(可以通過參數(shù) –XX:SurvivorRatio 來設(shè)定 。即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小)。堆大小 = 新生代+老年代。堆的大小可通過參數(shù)-Xms(堆的初識容量)、-Xmx(堆的最大容量)來指定。
內(nèi)存分配
內(nèi)存分配的方法有兩種:指針碰撞(Bump the Pointer)和空閑列表(Free List)
?內(nèi)存分配
?對象分配與簡要GC流程
方法區(qū)
介紹
線程共享,存儲已經(jīng)被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等等。
jdk1.7之前,HotSpot虛擬機對于方法區(qū)的實現(xiàn)稱之為“永久代”,Permanent Generation.
jdk1.8之后,HotSpot虛擬機對于方法區(qū)的實現(xiàn)稱之為“元空間”,Meta Space .
方法區(qū)是Java虛擬機規(guī)范中的定義,是一種規(guī)范,而永久代和元空間是 HotSpot虛擬機不同版本的兩種實現(xiàn)。
運行時常量池和字符串常量池
Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編 譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放。
?Java的線程與內(nèi)存
對象實例化
確認類元信息是否存在。當JVM接受到new指令時,首先在metaspace 內(nèi)檢查需要創(chuàng)建的類元信息是否存在。若不存在,那么在雙親委派模式下,使用當前類加載器以ClassLoader +包名+類名為key 進行查找對應(yīng)的.class 文件,如果沒有找到文件,則拋出ClassNotFoundException 異常,如果找到,則進行類加載并生成對應(yīng)的Class 類對象。
分配對象內(nèi)存。首先計算對象占用空間大小,如果實例成員變量是引用變量,僅分配引用變量空間即可,即4個字節(jié)大小,接著在堆中劃分一塊內(nèi)存給新對象。在分配內(nèi)存空間時,需要進行同步操作,比如采用CAS失敗重試,區(qū)域加鎖等方式保證分配操作的原子性。
設(shè)定默認值。成員變量值都需要設(shè)定默認值,即各種不同形式的零值。
設(shè)置對象頭。設(shè)置新對象的哈希碼,GC信息,鎖信息,對象所屬的類元信息等。這個過程的具體設(shè)置方式取決JVM實現(xiàn)。
執(zhí)行init 方法。初始化成員變量,執(zhí)行實例化代碼塊,調(diào)用類的構(gòu)造方法,并把堆內(nèi)對象的首地址賦值給引用變量。
垃圾回收
Java 會對內(nèi)存進行自動分配與回收管理,使上層業(yè)務(wù)更加安全,方便地使用內(nèi)存實現(xiàn)程序邏輯。GC 主要目的是清除不再使用的對象,自動釋放內(nèi)存。
GC是如何判斷對象是否可以被回收的呢?為了判斷對象是否存活,JVM引入了GC Roots.那么什么對象可以作為GC Roots呢?比如:類靜態(tài)屬性中引用的對象、常量引用的對象、虛擬棧中引用的對象、本地方法棧引用的對象等。
垃圾回收算法 主要是2種:引用計數(shù)法和根搜索算法(可達性算法)
垃圾回收器是實現(xiàn)垃圾回收算法并應(yīng)用與JVM環(huán)境中的內(nèi)存管理模塊。垃圾回收器有數(shù)十種,本文只介紹Serial、CMS、G1 三種
Serial回收器是一個主要應(yīng)用與YGC的垃圾回收器,采用串行單線程的方式完成GC任務(wù)。其中垃圾回收器的某個階段會暫停整個應(yīng)用程序的執(zhí)行(STW-Stop The Word)。FGC 的時間相對較長,頻翻FGC會嚴重影響應(yīng)用程序性能
CMS回收器是回收停頓時間比較短、目前比較常用的垃圾回收器。它通過初識標記(Initial Mark)、并發(fā)標記(Concurrent Makr)、重新標記(Remark)、并發(fā)清楚(Concurrent Sweep)四個步驟完成垃圾回收工作。由于CMS 采用的是“標記-清除算法” 因此會產(chǎn)生大量的空間碎片。可以通過配置
-XX:+UseCMSCompactAtFullCollection 參數(shù),強制JVM 在FGC 完成后對老年代進行壓縮,執(zhí)行一次空間碎片整理。同時空間碎片整理也會引發(fā)(STW)。
G1 垃圾回收可以通過-XX:UseG1GC 參數(shù)啟用。和CMS相比,G1具備壓縮功能,能避免碎片問題,G1的暫停時間更加可控。G1 采用的是“Mark-Copy”