天天看點

JVM工作原理簡述

JAVA之是以跨平台,是因為有JVM這麼一個編譯和運作機器,它令對于系統的操作對于使用者而言是黑盒的,使得開發人員更快速和更注重軟體功能的實作。然而,也因為jvm是黑盒,是以内部和底層具有不确定性,如果用狀态機來表示jvm,那麼jvm就是一種現役複制不确定的狀态機,因為它的狀态和表現跟系統、底層、硬體等等都有關系,進而狀态是不确定,如果在分布式應用中,jvm一直以來相容性都不是很好,這就是主要原因。盡管如此,就單一的系統而言,弄清楚jvm運作的來龍去脈,對于系統的運作至關重要。

了解jvm的運作原理具有以下幾點充分作用:

1、針對系統進行記憶體和垃圾回收監控

2、解決因記憶體溢出和洩露的問題

3、對系統進行優化

4、提升jvm和系統性能

jvm的運作原理有主要有三方面,其實這也是jvm的主要工作:

1、記憶體管理

2、執行流程

3、垃圾回收

在開始之前,有一些知識需要知道,廣義來講,jvm并不是指sun的hotspot,而是一個規範,是以不同廠商會根據規範實作不同的jvm,是以這些jvm的表現都不是一緻甚至相差甚遠。在jvm規範中,通常我們所能接觸的就是指令行參數了。

指令行參數

指令行參數分為三種,标準、非标準、非穩定

标準指令行參數會在jvm規範中明确列出,強制實作的選項,并且具有版本控制廢棄的管理通知。非标準的指令行參數不是規範強制并且可能沒有對應的通知,非穩定參數是特定調校的選項,同時也是非标準的。标準的選項可通過help指令檢視,非标準的選項通過-X為字首通路,非穩定的字首是-XX,通常對于布爾類型的選項,用+或-來設定true或者false,如-XX:+UseTLAB開啟線程記憶體緩沖配置設定。

記憶體劃分

jvm是具有記憶體自動配置設定和管理的架構,而記憶體管理自動化是解放勞動力的重要工具,可以對比C/C++,開發人員不需要管理記憶體,開發效率會比較高。

在jvm中,使用的記憶體分為兩類,線程共享記憶體和線程私有記憶體。

結合我們平時的代碼可以看出,線程共享的内容包括方法、執行個體對象、常量,分别對應共享記憶體中的方法區、堆區、常量池。

堆區

堆區通常是共享記憶體中最大的一塊,是以它也是GC重點關注區域。堆區可能是連續的也可能是不連續的,以及堆區的大小都會對GC造成相應的影響。-Xms和-Xmx設定堆的最小和最大值,如果堆記憶體大小超過最大值,則抛出OutOfMemoryError異常。

方法區

方法區存儲的是方法、類的結構資訊,而常量池也包含在内,除了我們代碼中所看到的靜态常量,這些常量還包括一些位元組碼内容和類初始化所需的特殊内容。通常情況下,jvm不會對方法區GC直到方法區大小不夠,即使GC也隻是針對常量池和類型,是以也被稱為永久區Permanent Generation,除了可以設定大小以外,還可以設定是否進行GC,如果超過大小,抛出OutOfMemoryError異常。

常量池

這裡說的常量池是運作時的,通常是位元組碼中的類的版本、描述,以及常量池表,這個表是一種符号表,在運作的時候将這些符号的引用變為直接符号。由此可以看出,加載類會使用常量池和方法區,如果類過多或常量過多,也會抛出OutOfMemoryError異常。

線程私有記憶體區

線程私有記憶體是被某一獨立線程獨占的,包括PC寄存器、java棧、本地方法棧

PC寄存器

這個寄存器是jvm内部的,而非實體寄存器,是以也可以看出,jvm的指令執行是基于棧架構的,所有的操作都是經過入棧出棧完成,為了確定線程安全,它被設定為線程私有的。通常,棧中存儲位元組碼指令位址,如果調用的是本地方法,即native方法,則是空值。會不會抛出OutOfMemoryError異常,jvm目前沒有明确規定。

java棧

java棧的顆粒度比PC寄存器大,存儲方法的局部變量、操作計數、方法傳回\方法出口等資訊。局部變量除了我們代碼所接觸的類型,還包括一種叫做returnAddress傳回位址類型,也是一種jvm規範的原始類型,但是開發人員并不能使用,實際上這種類型辨別一條位元組碼指令的操作嗎。java棧也會OutOfMemoryError異常,不過他也是可以動态擴充的。

本地方法棧

用于支援本地方法調用時使用,但是jvm沒有強制實作,和java棧類似。

執行流程

我們的代碼在IDE中或者通過CMD來執行即可看到執行結果,而實際上每次執行都會啟動和關閉jvm,這個過程是相當複雜的,下面羅列一下主要步驟。

對于sun的hotspot,launcher負責維護jvm的生命周期,包括啟動和結束關閉。就是我們在java目錄下看到的java.exe和javaw.exe,一個有控制台輸出,另一個沒有,用于執行GUI程式。

jvm的啟動初始化

1、解析指令行參數,設定記憶體大小和JIT編譯器,并且加載系統環境變量。

2、查找主類,并且調用本地方法JNI_CreateJavaVM建立jvm主線程。

3、當jvm初始化完成,就會加載主類,如果加載成功,則調用本地方法傳入參數,然後開始執行java的程式了。

其中調用本地方法JNI_CreateJavaVM建立jvm主線程,是jvm的啟動過程,實際上啟動器并非直接調用該本地方法,而是先用main()函數建立主線程,然後通過主線程調用javamain()函數調用該JNI_CreateJavaVM方法建立子線程來完成初始化并執行java程式。因為建立的主線程是作業系統配置設定的初始線程,為了更好的定制線程,通過在該線程上建立再初始線程來初始化jvm。

進一步細化JNI_CreateJavaVM函數的執行内容,主要流程如下:

1、檢查是否線程安全,也就是是否隻有一個線程調用此方法,一個線程隻能建立一個jvm執行個體。

2、初始化各個子產品,如日志、計數器、記憶體頁等。

3、加載核心庫并初始化線程庫

4、初始化全局資料,這步完成後就可以建立java子線程了

5、初始化類加載器、解析器、編譯器、GC等子產品。其中重要一點就是初始化universe類型,這種類型是java種一切類型的類型,是一種資料結構,所有java的存儲對象都用該類型類存儲。

6、加載并初始化基礎類庫,如Lang、System、reflect等包。

7、傳回給調用者。

通過上面的步驟,可以發現基礎類庫是在初始化階段完成加載的,這跟開發人員編寫的類庫加載順序是不同的。

jvm的關閉

當java程式結束,jvm會先檢查有無未處理的異常以及清理這些異常,然後調用本地方法斷開主線程跟本地方法接口的連接配接,如果可以斷開,說明已經沒有線程在運作了,則可以安全的關閉jvm。

和JNI_CreateJavaVM方法對應的是DestroyJavaVM方法,當jvm在啟動和運作時發生錯誤,根據嚴重程度會調用該方法關閉jvm,而在理想狀态下,即正常運作直到退出,也是調用DestroyJavaVM方法關閉并銷毀jvm。停止jvm按照以下主要步驟進行:

1、守護線程一緻等待,直到隻有一個非守護線程執行。

2、停止監控、計數器等線程。

3、移除目前線程,釋放保護頁。

4、釋放所有資源,傳回到調用者。

我們可以看出,當需要關閉jvm時,如果jvm中仍有線程在運作,是無法強制關閉的,這就是為什麼我們很多代碼的運作出現異常後,重複的調試導緻有多個背景jvm在運作卻不能自動結束而要手動關閉。

類加載機制

在前面說到,開發人員使用的類和基礎類庫并非同一時間加載的,這是有原因的。類的加載由類加載器來完成,包括加載、連接配接、初始化三個階段。完成加載後就可以通過new來建立類的執行個體對象了。類的加載可以了解為根據類的位元組碼檔案全路徑名讀取後轉換為與目标類型一緻的Class類型,并且是可以動态加載的。

加載類由類加載器完成,加載器分為兩種,一種是Bootstrap Classloader引導加載器,另一種是User-defined Classloader使用者自定義加載器,使用者自定義加載器預設又分為ExtClassloader和AppClassloader。

引導加載器是C++編寫的,負責完成lib目錄裡的類加載,也就是前面所說的基礎類庫,而ExtClassloader和AppClassloader是java編寫的,分别負責加載lib/ext目錄和ClassPath系統路徑中的類型。他們都是Classloader的子類,我們也可以通過繼承父類來實作自己的類加載器。

父類委托模式

通過查閱類關系樹可以發現,AppClassloader是ExtClassloader的子類,而ExtClassloader則是Classloader的子類,java規範要求自定義的類加載器都派生與父類,并且在進行類加載的時候,都要委托給直接上級父類執行加載,這就是父類委托模式(parents delegation model),國内很多翻譯為雙親委托模式,但是你會發現是多親模式,是以我認為父類委托更為合适。

父類委托模式在執行時,子類始終會委托父類加載,一級一級的向上請求,知道最後唯一的超類來進行加載,如果父類無法加載,再一級一級的退回到子類進行加載,這樣就不會重複加載相同的類了。

為什麼要使用父類委托模式?因為類的加載必須是一次性不可重複的,試想一下,如果基礎類庫中的類可以重複加另一個類來替換原來的類,那是多麼嚴重的安全隐患,為了避免這一點,基礎類庫都是由C++編寫的啟動加載器來加載,但是為了兼顧擴充性,是以除了基礎類庫,其他的類都可以通過使用者加載器來加載,那麼為了避免但不強制要求避免重複加載的情況發生,java規範就采取并建議我們按照父類委托的方式實作類加載器。

類的加載過程

前面說到,類先經過類加載器将位元組碼檔案轉換為Class對象,但是這個時候并不能使用它,此時的類結構資訊存儲在方法區内,還需要對其進行驗證,結構資訊是否有效合法,一旦通過驗證,就會為類中的靜态變量配置設定記憶體空間并初始化值,這些準備工作完成後,還需将類結構中的符号和常量表的符号進行解析轉為直接引用,這時候的類才具有執行能力。最後的工作就是初始化了,也就是我們代碼中在new一個對象之前會執行的static代碼塊。

垃圾回收機制

jvm的垃圾回收包括記憶體動态配置設定和記憶體回收兩大塊。記憶體的配置設定和垃圾回收是息息相關的,記憶體配置設定的方式一定程度上決定采取何種垃圾收集器和收集算法。

前面說到,堆記憶體可以是連續也可以是不連續的,也是GC的重點區域,但正由于這種分布的不确定性,該GC帶來很大麻煩。首先針對連續的情況。

指針碰撞

通過前面講述的jvm啟動過程,我們知道建立對象就需要在堆記憶體中劃分出一部分來存儲對象,如果此時的記憶體是規整的,那麼将空閑的和已使用的各放置一邊,兩部分的邊界處用一個指針标記,當新增對象記憶體配置設定,就将指針偏移相應的位置,下一次配置設定記憶體隻需要知道最後指針偏移的位置開始配置設定記憶體并更新指針偏移量即可,這種方式就是指針碰撞(bump the pointer)。

空閑清單

然而,需要面臨的一個問題首先不是規整問題,而是線程安全,如果對指針的操作加鎖,必然會降低性能。并且如果堆不是連續的,指針碰撞就變得很棘手,此時還有一種解決辦法,就是通過一張表記錄下所有空閑的記憶體,每當配置設定記憶體就更新表上的記錄,這種方式就是空閑清單(free list)。

不管呢種方式,都必須解決線程安全,對于指針碰撞,為了滿足規整的先決條件,這就要求GC收集器具有壓縮規整功能,如serial、par等收集器,而采用mark-sweep的cms這種收集器則不支援規整,因為他就是通過空閑清單方式來整理的記憶體的。配置設定記憶體就需要對記憶體指針進行操作,如何確定指針的使用是線程安全的?一種做法是用過CAS原子操作來實作,也就是所謂的失敗重試保證更新原子性。還有一種做法就是TLAB(本地線程緩沖),即在堆記憶體中事先劃分一塊線程獨占的私有記憶體,這樣線程就可以互不幹涉的建立對象了,如果TLAB不夠用,再已加鎖的方式配置設定TLAB,并且對象的初始化還可以提前進行。

分代劃分收集機制

目前大部分的GC都是采用分代收集算法的,換而言之,也就是記憶體是分代劃分的。這當中的設計有很多複雜和嚴格的要求,首先對算法絕對精确,不能造成誤删和誤讀,還要保證沒用的對象及時回收,以及如何處理産生的碎片和系統停頓開銷等。涉及的名額和算法,就在另一篇中單獨闡述了。

待續......

下一篇: JVM GC流程

繼續閱讀