「讀書分享」- 走進JVM,深入理解JVM

---云開方見日,潮盡爐峰出。揭開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”

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內(nèi)容