阿里P8架構師帶你了解:JVM運行原理詳解(超詳細)

1.JVM簡析:

作為一名Java使用者,掌握JVM的體系結(jié)構也是很有必要的。

說起Java,我們首先想到的是Java編程語言,然而事實上,Java是一種技術,它由四方面組成:Java編程語言、Java類文件格式、Java虛擬機和Java應用程序接口(Java API)。它們的關系如下圖所示:

運行期環(huán)境代表著Java平臺,開發(fā)人員編寫Java代碼(.java文件),然后將之編譯成字節(jié)碼(.class文件),再然后字節(jié)碼被裝入內(nèi)存,一旦字節(jié)碼進入虛擬機,它就會被解釋器解釋執(zhí)行,或者是被即時代碼發(fā)生器有選擇的轉(zhuǎn)換成機器碼執(zhí)行。

Java平臺由Java虛擬機和Java應用程序接口搭建,Java語言則是進入這個平臺的通道,用Java語言編寫并編譯的程序可以運行在這個平臺上。這個平臺的結(jié)構如下圖所示:

在Java平臺的結(jié)構中, 可以看出Java虛擬機(JVM) 處在核心的位置,是程序與底層操作系統(tǒng)和硬件無關的關鍵。它的下方是移植接口,移植接口由兩部分組成:適配器和Java操作系統(tǒng), 其中依賴于平臺的部分稱為適配器;JVM 通過移植接口在具體的平臺和操作系統(tǒng)上實現(xiàn);在JVM 的上方是Java的基本類庫和擴展類庫以及它們的API, 利用Java API編寫的應用程序(application) 和小程序(Java applet) 可以在任何Java平臺上運行而無需考慮底層平臺, 就是因為有Java虛擬機(JVM)實現(xiàn)了程序與操作系統(tǒng)的分離,從而實現(xiàn)了Java 的平臺無關性。

JVM在它的生存周期中有一個明確的任務,那就是運行Java程序,因此當Java程序啟動的時候,就產(chǎn)生JVM的一個實例;當程序運行結(jié)束的時候,該實例也跟著消失了。

下面我們從JVM的基本概念和運過程程這兩個方面入手來對它進行深入的研究。

2.JVM基本概念

2.1 基本概念:

JVM是可運行Java代碼的假想計算機 ,包括一套字節(jié)碼指令集、一組寄存器、一個棧、一個垃圾回收,堆 和 一個存儲方法域。JVM是運行在操作系統(tǒng)之上的,它與硬件沒有直接的交互。

2.2 運行過程:

我們都知道Java源文件,通過編譯器,能夠生產(chǎn)相應的.Class文件,也就是字節(jié)碼文件,而字節(jié)碼文件又通過Java虛擬機中的解釋器,編譯成特定機器上的機器碼 。

也就是如下:

? Java源文件—->編譯器—->字節(jié)碼文件

? 字節(jié)碼文件—->JVM—->機器碼

每一種平臺的解釋器是不同的,但是實現(xiàn)的虛擬機是相同的,這也就是Java為什么能夠跨平臺的原因了 ,當一個程序從開始運行,這時虛擬機就開始實例化了,多個程序啟動就會存在多個虛擬機實例。程序退出或者關閉,則虛擬機實例消亡,多個虛擬機實例之間數(shù)據(jù)不能共享。

2.3 三種JVM:

? Sun公司的HotSpot;

? BEA公司的JRockit;

? IBM公司的J9 JVM;

在JDK1.7及其以前我們所使用的都是Sun公司的HotSpot,但由于Sun公司和BEA公司都被oracle收購,jdk1.8將采用Sun公司的HotSpot和BEA公司的JRockit兩個JVM中精華形成jdk1.8的JVM。

3.JVM的體系結(jié)構

3.1 Class Loader類加載器

負責加載 .class文件,class文件在文件開頭有特定的文件標示,并且ClassLoader負責class文件的加載等,至于它是否可以運行,則由Execution Engine決定。

3.2 Native Interface本地接口

本地接口的作用是融合不同的編程語言為Java所用,它的初衷是融合C/C++程序,Java誕生的時候C/C++橫行的時候,要想立足,必須有調(diào)用C/C++程序,于是就在內(nèi)存中專門開辟了一塊區(qū)域處理標記為native的代碼,它的具體作法是Native Method Stack中登記native方法,在Execution Engine執(zhí)行時加載native libraies。

目前該方法使用的越來越少了,除非是與硬件有關的應用,比如通過Java程序驅(qū)動打印機,或者Java系統(tǒng)管理生產(chǎn)設備,在企業(yè)級應用中已經(jīng)比較少見, 因為現(xiàn)在的異構領域間的通信很發(fā)達,比如可以使用Socket通信,也可以使用Web Service等。

3.3 Execution Engine 執(zhí)行引擎

執(zhí)行包在裝載類的方法中的指令,也就是方法。

3.4 Runtime data area 運行數(shù)據(jù)區(qū)

虛擬機內(nèi)存或者Jvm內(nèi)存,沖整個計算機內(nèi)存中開辟一塊內(nèi)存存儲Jvm需要用到的對象,變量等,運行區(qū)數(shù)據(jù)有分很多小區(qū),分別為:方法區(qū),虛擬機棧,本地方法棧,堆,程序計數(shù)器。

4. Runtime data area 運行數(shù)據(jù)區(qū)詳解(棧管運行,堆管存儲):

說明:JVM調(diào)優(yōu)主要就是優(yōu)化 Heap堆 和 Method Area 方法區(qū)。

4.1 Native Method Stack 本地方法棧

它的具體做法是Native Method Stack中登記native方法,在Execution Engine執(zhí)行時加載native libraies。

4.2 PC Register程序計數(shù)器

每個線程都有一個程序計算器,就是一個指針,指向方法區(qū)中的方法字節(jié)碼(下一個將要執(zhí)行的指令代碼),由執(zhí)行引擎讀取下一條指令,是一個非常小的內(nèi)存空間,幾乎可以忽略不記。

4.3 Method Area方法區(qū)

方法區(qū)是被所有線程共享,所有字段和方法字節(jié)碼,以及一些特殊方法如構造函數(shù),接口代碼也在此定義。簡單說,所有定義的方法的信息都保存在該區(qū)域,此區(qū)域?qū)儆诠蚕韰^(qū)間。

靜態(tài)變量,常量,類信息(構造方法/接口定義),運行時常量池存在方法區(qū)中;但是實例變量存在堆內(nèi)存中,和方法區(qū)無關。

4.4 Stack 棧

問題來了

4.4.1 棧是什么?

棧也叫棧內(nèi)存,主管Java程序的運行,是在線程創(chuàng)建時創(chuàng)建,它的生命期是跟隨線程的生命期,線程結(jié)束棧內(nèi)存也就釋放,對于棧來說不存在垃圾回收問題,只要線程一結(jié)束該棧就Over,生命周期和線程一致,是線程私有的。

基本類型的變量和對象的引用變量都是在函數(shù)的棧內(nèi)存中分配。

4.4.2 棧存儲什么?

棧幀中主要保存3類數(shù)據(jù):

本地變量(Local Variables):輸入?yún)?shù)和輸出參數(shù)以及方法內(nèi)的變量;

棧操作(Operand Stack):記錄出棧、入棧的操作;

棧幀數(shù)據(jù)(Frame Data):包括類文件、方法等等。

4.4.3 棧運行原理

棧中的數(shù)據(jù)都是以棧幀(Stack Frame)的格式存在,棧幀是一個內(nèi)存區(qū)塊,是一個數(shù)據(jù)集,是一個有關方法和運行期數(shù)據(jù)的數(shù)據(jù)集,當一個方法A被調(diào)用時就產(chǎn)生了一個棧幀F(xiàn)1,并被壓入到棧中, A方法又調(diào)用了B方法,于是產(chǎn)生棧幀F(xiàn)2也被壓入棧, B方法又調(diào)用了C方法,于是產(chǎn)生棧幀F(xiàn)3也被壓入?!?依次執(zhí)行完畢后,先彈出后進......F3棧幀,再彈出F2棧幀,再彈出F1棧幀。

遵循“先進后出”/“后進先出”原則。

4.5 Heap 堆

堆這塊區(qū)域是JVM中最大的,應用的對象和數(shù)據(jù)都是存在這個區(qū)域,這塊區(qū)域也是線程共享的,也是 gc 主要的回收區(qū),一個 JVM 實例只存在一個堆類存,堆內(nèi)存的大小是可以調(diào)節(jié)的。類加載器讀取了類文件后,需要把類、方法、常變量放到堆內(nèi)存中,以方便執(zhí)行器執(zhí)行,堆內(nèi)存分為三部分:

4.5.1 新生區(qū)

新生區(qū)是類的誕生、成長、消亡的區(qū)域,一個類在這里產(chǎn)生,應用,最后被垃圾回收器收集,結(jié)束生命。新生區(qū)又分為兩部分:伊甸區(qū)(Eden space)和幸存者區(qū)(Survivor pace),所有的類都是在伊甸區(qū)被new出來的。幸存區(qū)有兩個:0區(qū)(Survivor 0 space)和1區(qū)(Survivor 1 space)。當伊甸園的空間用完時,程序又需要創(chuàng)建對象,JVM的垃圾回收器將對伊甸園進行垃圾回收(Minor GC),將伊甸園中的剩余對象移動到幸存0區(qū)。若幸存0區(qū)也滿了,再對該區(qū)進行垃圾回收,然后移動到1區(qū)。那如果1去也滿了呢?再移動到養(yǎng)老區(qū)。若養(yǎng)老區(qū)也滿了,那么這個時候?qū)a(chǎn)生Major GC(FullGCC),進行養(yǎng)老區(qū)的內(nèi)存清理。若養(yǎng)老區(qū)執(zhí)行Full GC 之后發(fā)現(xiàn)依然無法進行對象的保存,就會產(chǎn)生OOM異常“OutOfMemoryError”。

如果出現(xiàn)java.lang.OutOfMemoryError: Java heap space異常,說明Java虛擬機的堆內(nèi)存不夠。原因有二:

a.Java虛擬機的堆內(nèi)存設置不夠,可以通過參數(shù)-Xms、-Xmx來調(diào)整。

b.代碼中創(chuàng)建了大量大對象,并且長時間不能被垃圾收集器收集(存在被引用)。

4.5.2 養(yǎng)老區(qū)

養(yǎng)老區(qū)用于保存從新生區(qū)篩選出來的 JAVA 對象,一般池對象都在這個區(qū)域活躍。

4.5.3 永久區(qū)

永久存儲區(qū)是一個常駐內(nèi)存區(qū)域,用于存放JDK自身所攜帶的 Class,Interface 的元數(shù)據(jù),也就是說它存儲的是運行環(huán)境必須的類信息,被裝載進此區(qū)域的數(shù)據(jù)是不會被垃圾回收器回收掉的,關閉 JVM 才會釋放此區(qū)域所占用的內(nèi)存。

如果出現(xiàn)java.lang.OutOfMemoryError: PermGen space,說明是Java虛擬機對永久代Perm內(nèi)存設置不夠。 原因有二:

a. 程序啟動需要加載大量的第三方jar包。例如:在一個Tomcat下部署了太多的應用。

b. 大量動態(tài)反射生成的類不斷被加載,最終導致Perm區(qū)被占滿。

說明:

1.堆的分配參數(shù)

-Xmn

- 設置新生代大小

-XX:NewRatio

- 新生代(eden+2*s)和老年代(不包含永久區(qū))的比值

- 4 表示 新生代:老年代=1:4,即年輕代占堆的1/5

-XX:SurvivorRatio

- 設置兩個Survivor區(qū)和eden的比

- 8表示 兩個Survivor :eden=2:8,即一個Survivor占年輕代的1/10

2.堆的分配參數(shù) – 總結(jié)

根據(jù)實際事情調(diào)整新生代和幸存代的大小

官方推薦新生代占堆的3/8

幸存代占新生代的1/10

在OOM時,記得Dump出堆,確??梢耘挪楝F(xiàn)場問題

5.說明

Jdk1.6及之前:常量池分配在永久代 ;

Jdk1.7:有,但已經(jīng)逐步“去永久代” ;

Jdk1.8及之后:無(java.lang.OutOfMemoryError: PermGen space,這種錯誤將不會出現(xiàn)在JDK1.8中 );

5.1 在JDK1.7 中

5.2 在JDK 1.8 中

本文到這里就結(jié)束了,喜歡的小伙伴可以點擊關注轉(zhuǎn)發(fā)+收藏喲

原文鏈接:https://blog.csdn.net/Luomingkui1109/article/details/72820232

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容