jvm最全介紹

0、Java 對內存的劃分:

 

 圖1/10

Java虛擬機規範將物理內存(主內存和CPU中的緩存、寄存器)劃分爲程序計數器、Java 虛擬機棧、本地方法棧、Java 堆、方法區五個區域,但並沒有規定這些區域的具體實現,在其他地方聽到的一些名詞(如永久代、元空間等,這些都是方法區的具體實現)可能都是這些區域具體的實現,這點要特別注意,別被這些概念搞暈。

 

各個區域的特點如下表:

 

 圖2/10

1、類加載器:

 

類加載器分爲Bootstrap、Extension ClassLoader(Java9 中是 Platform ClassLoader)、Application ClassLoader,級別也是從高到低。

 

可以調用類加載器對象的 getParent() 方法查找該級加載器的上一級加載器,也成爲父類加載器。

 

 圖3/10

此處不翻譯了,翻譯後就變味了,尤其是下面的 Parents Delegation Model 翻譯爲雙親委派模型很不恰當。

 

字節碼文件加載到內存中,纔可以實例化出類,而類加載器就是負責加載 Java 類的。低級別的類加載器在加載一個類時會先詢問上一級的類加載器,直到詢問到頂級的類加載器(Bootstrap),如果頂級的類加載器可以加載就加載該類,否則向下嘗試是否可以加載該類,也即是如果上一級類加載器能加載的就用上一級加載(複用上一級的類加載器),用不了再用自身的類加載器加載,這也就是口口相傳卻是翻譯很不恰當的雙親委派模型。這樣做可以使類加載更加安全,避免加載和標準 Java 類同包同名的類破壞虛擬機。

 

可以根據需要繼承 Application ClassLoader 實現自定義類加載器,隔離加載器、修改類的加載方式、擴展加載源、防止源碼泄露。

 

2、類加載的過程:

 

類加載是將字節碼文件實例化成 Class 對象並進行相關初始化的過程。類加載包括類的加載(Load)、類的鏈接(Link)、類的初始化(init)三個步驟。

 

類的加載是將字節碼文件以二進制流的方式讀取到內存中並轉化爲特定的數據結構,檢查 cafe baby 這個魔法數(是不是Java文件的標誌),是否有父類等,創建類對應的 Class 對象。

 

類的鏈接又分爲驗證、準備、解析三個階段,驗證階段是進行更加詳細的校驗,如類型是否正確,靜態變量是否合理等;準備階段是爲類的靜態變量分配內存空間,並設定默認值;解析階段是保證類和類之間相互引用的正確性,完成類在內存中的結構佈局。

 

類的初始化並不是初始化對象,而是根據代碼中的值初始化類的靜態變量值,類的靜態變量的初始化方式也有直接在聲明時指定值和在靜態代碼塊中指定值兩種方式。

 

3、訪問對象的兩種方式:

 

Java虛擬機棧中的局部變量表存放的數據除了基本的數據類型外,還有對象的引用類型(reference),這關係到如何訪問一個對象。

 

在不同的虛擬機中,對象的訪問方式也是不同的,主流的訪問方式有使用句柄和直接指針兩種。

 

使用句柄:

 

 圖4/10

使用句柄是在 Java 堆中劃分出一塊區域作爲句柄池,句柄池中存放對象的實例數據和類型數據(類相關的信息)的地址,reference 中存放的是對象對應句柄中的地址,這是一種間接訪問對象方式。

 

直接指針:

 

 圖5/10

直接指針是reference中直接存放對象的地址,但 Java 堆需要考慮如何存放訪問對象類型的指針。

 

兩種方式其實各有優劣,如下表:

 

 圖6/10

4、判斷對象是否可以回收的算法:

 

垃圾回收之前需要判斷對象是否可以回收,常見的判斷算法有引用計數算法和可達性分析算法。

 

引用計數算法:

 

每個對象都有對應的引用計數器,當有一個地方引用該對象時,就將引用計數器的值加1,當引用失效時,就將引用計數器的值減1,當計數器的值爲0時,表示對象沒有引用,可以被回收了。

 

缺點:看起來簡單高效,但是有循環引用問題。如果兩個對象中包含對方的引用就會產生循環引用問題,導致垃圾收集器不能回收對象。

 

可達性分析算法:

 

如果對象與GC Roots 之間沒有直接或間接的應用關係,就可以被回收了。常見的 GC Roots 對象包括虛擬機棧(棧幀本地變量表)中引用的對象、方法區中靜態屬性引用的對象、方法區常量引用的對象、本地方法棧中(Native 方法)引用的對象。GC Roots,是一個特殊的對象,且絕對不能被其他對象引用,不然也會像引用計數算法那樣有循環引用的問題。

 

5、常見的垃圾回收算法:

 

標記-清除算法

 

最基本的垃圾回收算法,後續的算法都是對它的改進。

 

首先標記出需要回收的對象,再將標記出的區域內容清除。

 

缺點是:標記時的查找效率,清除時產生內存碎片。

 

 圖7/10

標記-複製算法

 

將內存區域劃分爲兩塊,每次只使用一塊,垃圾回收時,標記正在使用的內存區域,將存活的對象複製到另一塊內存區域,再將原來的那一塊內存區域一次性清除。避免了內存碎片的產生,但不適合存活時間長的對象。

 

缺點:浪費了一半的內存空間,當對象存活率高時,進行大量的複製操作,效率不高。

 

 圖8/10

標記-整理算法

 

標記過程和標記-除算法相同,垃圾回收時,是將存活的對象向同一端移動,再清除這之外的內存區域,這樣就使得對象佔用的內存區域連續,避免了內存碎片的產生。

 

 圖9/10

分代收集算法

 

根據對象存活時間的長短,將堆內存分爲新生代和老生代,存活時間短的對象放在新生代區域,存活時間長的大對象(如對象數組)放在老生代區域。新生代和老生代的比例是 1 : 2,新生代又分爲一個 Eden 區和兩個 Survivor 區。新生代使用標記-複製算法,老生代使用標記-清除算法或標記-整理算法,這樣最大發揮各自算法的優勢。

 

 圖10/10

6、常見的垃圾回收器:

 

Serial 回收器

 

Serial 採取 “複製算法” 實現,如果是在單 CPU 環境下,Serial 收集器沒有線程交互的開銷,理論上是可以獲得最高的單線程執行效率,STW 的時間也可以控制在幾十到幾百毫秒內,這個時間是完全可以接受的。

 

Serial Old (PS MarkSweep)回收器

 

Serial Old 收集器 是 Serial 收集器的老年代版本,同樣也是一個單線程收集器,使用了 “標記-整理算法”。

 

ParNew 回收器

 

ParNew 收集器實際上就是 Serial 收集器的多線程版本,收集算法、STW、對象分配的規則、回收策略等都與 Serial 收集器完全一樣,兩者相同的代碼很多。ParNew 收集器雖然有多線程優勢,但在單 CPU 和多 CPU 環境下,效果並不一定會比 Serial 好,至少在單 CPU 環境下是肯定不如的 Serial 的。

 

Parallel Scavenge 回收器

 

Parallel Scavenge收集器和 ParNew 收集器很像,也是一個新生代收集器,也是使用複製算法,並且還是並行的多線程的收集器。相比於 ParNew 收集器,Parallel Scavenge收集器可以更加精準的控制 CPU 的吞吐量和 STW 的時間,對於交互不多的任務可以更快地完成。

 

Parallel Old 回收器

 

Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多線程和 “標記-整理算法”。在 Parallel Old 收集器出現之間,選擇了 Parallel Scavenge 收集器作爲新生代的收集器,就只能選擇 Serial Old 收集器作爲老生代收集器,這樣肯定就是對多 CPU 的浪費,所以 Parallel Scavenge收集器 + Parallel Old 收集器,對於多 CPU 環境吞吐量要求高的環境,算是強強聯合。

 

CMS 回收器

 

CMS (Concurrent Mark Sweep)收集器從英文名字上看就是基於 “標記-清除算法” 實現的,並且還有併發的特點,它是一種以縮短 STW 的時間爲目標的收集器,對於一些重視服務響應速度的網站,肯定是 STW 越短,用戶體驗越好,但是缺點是會在垃圾收集結束後產生大量的空間碎片。

 

通過初始標記(Initial Mark)、併發標記(Concurrent Mark)、重新標記(Remark)、併發清除(Concurrent Sweep)四個步驟完成垃圾回收。

 

G1 回收器

 

G1 收集器是目前最先進的收集器,它是基於 “標記-複製算法” 實現的,所以不會產生內存碎片,並且也可以精準地控制 STW 的時間。G1 收集器對於新生代和老年代都是適用的,優先回收垃圾最多的區域。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章