深入理解 JVM 垃圾回收機(jī)制及其實(shí)現(xiàn)原理
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
76套java從入門到精通實(shí)戰(zhàn)課程分享
前言
對(duì)于 JVM 來說,我們都不陌生,其是 Java Virtual Machine(Java 虛擬機(jī))的縮寫,它也是一個(gè)虛構(gòu)出來的計(jì)算機(jī),是通過在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來實(shí)現(xiàn)的。JVM 有自己完善的硬件架構(gòu),如處理器、堆棧等,還具有相應(yīng)的指令系統(tǒng),其本質(zhì)上就是一個(gè)程序,當(dāng)它在命令行上啟動(dòng)的時(shí)候,就開始執(zhí)行保存在某字節(jié)碼文件中的指令。
Java 語言的可移植性就是建立在 JVM 的基礎(chǔ)之上的,任何平臺(tái)只要裝有針對(duì)于該平臺(tái)的 Java 虛擬機(jī),字節(jié)碼文件(.class)就可以在該平臺(tái)上運(yùn)行,這就是“一次編譯,多次運(yùn)行”。除此之外,作為 Java 語言最重要的特性之一的自動(dòng)垃圾回收機(jī)制,也是基于 JVM 實(shí)現(xiàn)的。那么,自動(dòng)垃圾回收機(jī)制到底是如何實(shí)現(xiàn)的呢?在本文中,就讓我們一探究竟。
垃圾
什么是垃圾?
在 JVM 進(jìn)行垃圾回收之前,首先就是判斷哪些對(duì)象是垃圾,也就是說,要判斷哪些對(duì)象是可以被銷毀的,其占有的空間是可以被回收的。根據(jù) JVM 的架構(gòu)劃分,我們知道, 在 Java 世界中,幾乎所有的對(duì)象實(shí)例都在堆中存放,所以垃圾回收也主要是針對(duì)堆來進(jìn)行的。
在 JVM 的眼中,垃圾就是指那些在堆中存在的,已經(jīng)“死亡”的對(duì)象。而對(duì)于“死亡”的定義,我們可以簡(jiǎn)單的將其理解為“不可能再被任何途徑使用的對(duì)象”。那怎樣才能確定一個(gè)對(duì)象是存活還是死亡呢?這就涉及到了垃圾判斷算法,其主要包括引用計(jì)數(shù)法和可達(dá)性分析法。
垃圾判斷算法
引用計(jì)數(shù)法
在這種算法中,假設(shè)堆中每個(gè)對(duì)象(不是引用)都有一個(gè)引用計(jì)數(shù)器。當(dāng)一個(gè)對(duì)象被創(chuàng)建并且初始化賦值后,該對(duì)象的計(jì)數(shù)器的值就設(shè)置為 1,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器的值就加 1,例如將對(duì)象 b 賦值給對(duì)象 a,那么 b 被引用,則將 b 引用對(duì)象的計(jì)數(shù)器累加 1。
反之,當(dāng)引用失效時(shí),例如一個(gè)對(duì)象的某個(gè)引用超過了生命周期(出作用域后)或者被設(shè)置為一個(gè)新值時(shí),則之前被引用的對(duì)象的計(jì)數(shù)器的值就減 1。而那些引用計(jì)數(shù)為 0 的對(duì)象,就可以稱之為垃圾,可以被收集。
特別地,當(dāng)一個(gè)對(duì)象被當(dāng)做垃圾收集時(shí),它引用的任何對(duì)象的計(jì)數(shù)器的值都減 1。
優(yōu)點(diǎn):引用計(jì)數(shù)法實(shí)現(xiàn)起來比較簡(jiǎn)單,對(duì)程序不被長(zhǎng)時(shí)間打斷的實(shí)時(shí)環(huán)境比較有利。
缺點(diǎn):需要額外的空間來存儲(chǔ)計(jì)數(shù)器,難以檢測(cè)出對(duì)象之間的循環(huán)引用。
可達(dá)性分析法
可達(dá)性分析法也被稱之為根搜索法,可達(dá)性是指,如果一個(gè)對(duì)象會(huì)被至少一個(gè)在程序中的變量通過直接或間接的方式被其他可達(dá)的對(duì)象引用,則稱該對(duì)象就是可達(dá)的。更準(zhǔn)確的說,一個(gè)對(duì)象只有滿足下述兩個(gè)條件之一,就會(huì)被判斷為可達(dá)的:
對(duì)象是屬于根集中的對(duì)象
對(duì)象被一個(gè)可達(dá)的對(duì)象引用
在這里,我們引出了一個(gè)專有名詞,即根集,其是指正在執(zhí)行的 Java 程序可以訪問的引用變量(注意,不是對(duì)象)的集合,程序可以使用引用變量訪問對(duì)象的屬性和調(diào)用對(duì)象的方法。在 JVM 中,會(huì)將以下對(duì)象標(biāo)記為根集中的對(duì)象,具體包括:
虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
方法區(qū)中的常量引用的對(duì)象
方法區(qū)中的類靜態(tài)屬性引用的對(duì)象
本地方法棧中 JNI(Native 方法)的引用對(duì)象
活躍線程(已啟動(dòng)且未停止的 Java 線程)
根集中的對(duì)象稱之為GC Roots,也就是根對(duì)象。可達(dá)性分析法的基本思路是:將一系列的根對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈,如果一個(gè)對(duì)象到根對(duì)象沒有任何引用鏈相連,那么這個(gè)對(duì)象就不是可達(dá)的,也稱之為不可達(dá)對(duì)象。

如上圖所示,形象的展示了可達(dá)對(duì)象與不可達(dá)對(duì)象的示例,其中灰色的對(duì)象都是不可達(dá)對(duì)象,表示可以被垃圾收集的對(duì)象。在可達(dá)性分析法中,對(duì)象有兩種狀態(tài),那么是可達(dá)的、要么是不可達(dá)的,在判斷一個(gè)對(duì)象的可達(dá)性的時(shí)候,就需要對(duì)對(duì)象進(jìn)行標(biāo)記。關(guān)于標(biāo)記階段,有幾個(gè)關(guān)鍵點(diǎn)是值得我們注意的,分別是:
開始進(jìn)行標(biāo)記前,需要先暫停應(yīng)用線程,否則如果對(duì)象圖一直在變化的話是無法真正去遍歷它的。暫停應(yīng)用線程以便 JVM 可以盡情地收拾家務(wù)的這種情況又被稱之為安全點(diǎn)(Safe Point),這會(huì)觸發(fā)一次 Stop The World(STW)暫停。觸發(fā)安全點(diǎn)的原因有許多,但最常見的應(yīng)該就是垃圾回收了。
安全點(diǎn)的選定基本上是以程序“是否具有讓程序長(zhǎng)時(shí)間執(zhí)行的特征”為標(biāo)準(zhǔn)進(jìn)行選定的?!伴L(zhǎng)時(shí)間執(zhí)行”的最明顯特征就是指令序列復(fù)用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等,所以具有這些功能的指令才會(huì)產(chǎn)生安全點(diǎn)。對(duì)于安全點(diǎn),另一個(gè)需要考慮的問題就是如何在 GC 發(fā)生時(shí)讓所有線程(這里不包括執(zhí)行 JNI 調(diào)用的線程)都“跑”到最近的安全點(diǎn)上再停頓下來。兩種解決方案:
搶先式中斷(Preemptive Suspension):搶先式中斷不需要線程的執(zhí)行代碼主動(dòng)去配合,在 GC 發(fā)生時(shí),首先把所有線程全部中斷,如果發(fā)現(xiàn)有線程中斷的地方不在安全點(diǎn)上,就恢復(fù)線程,讓它“跑”到安全點(diǎn)上?,F(xiàn)在幾乎沒有虛擬機(jī)采用這種方式來暫停線程從而響應(yīng) GC 事件。
主動(dòng)式中斷(Voluntary Suspension):主動(dòng)式中斷的思想是當(dāng) GC 需要中斷線程的時(shí)候,不直接對(duì)線程操作,僅僅簡(jiǎn)單地設(shè)置一個(gè)標(biāo)志,各個(gè)線程執(zhí)行時(shí)主動(dòng)去輪詢這個(gè)標(biāo)志,發(fā)現(xiàn)中斷標(biāo)志為真時(shí)就自己中斷掛起。輪詢標(biāo)志地地方和安全點(diǎn)是重合的,另外再加上創(chuàng)建對(duì)象需要分配內(nèi)存的地方。
暫停時(shí)間的長(zhǎng)短并不取決于堆內(nèi)對(duì)象的多少也不是堆的大小,而是存活對(duì)象的多少。因此,調(diào)高堆的大小并不會(huì)影響到標(biāo)記階段的時(shí)間長(zhǎng)短。
在根搜索算法中,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過程:
如果對(duì)象在進(jìn)行根搜索后發(fā)現(xiàn)沒有與根對(duì)象相連接的引用鏈,那它會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選。篩選的條件是此對(duì)象是否有必要執(zhí)行 finalize()方法(可看作析構(gòu)函數(shù),類似于 OC 中的dealloc,Swift 中的deinit)。當(dāng)對(duì)象沒有覆蓋finalize()方法,或finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過,虛擬機(jī)將這兩種情況都視為沒有必要執(zhí)行。
如果該對(duì)象被判定為有必要執(zhí)行finalize()方法,那么這個(gè)對(duì)象將會(huì)被放置在一個(gè)名為F-Queue的隊(duì)列中,并在稍后由一條由虛擬機(jī)自動(dòng)建立的、低優(yōu)先級(jí)的Finalizer線程去執(zhí)行finalize()方法。finalize()方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì)(因?yàn)橐粋€(gè)對(duì)象的finalize()方法最多只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次),稍后 GC 將對(duì)F-Queue中的對(duì)象進(jìn)行第二次小規(guī)模的標(biāo)記,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中讓該對(duì)象重新引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可。而如果對(duì)象這時(shí)還沒有關(guān)聯(lián)到任何鏈上的引用,那它就會(huì)被回收掉。
GC 判斷對(duì)象是否可達(dá)看的是強(qiáng)引用。
當(dāng)標(biāo)記階段完成后,GC 開始進(jìn)入下一階段,刪除不可達(dá)對(duì)象。當(dāng)然,可達(dá)性分析法有優(yōu)點(diǎn)也有缺點(diǎn),
優(yōu)點(diǎn):可以解決循環(huán)引用的問題,不需要占用額外的空間
缺點(diǎn):多線程場(chǎng)景下,其他線程可能會(huì)更新已經(jīng)訪問過的對(duì)象的引用
在上面的介紹中,我們多次提到了“引用”這個(gè)概念,在此我們不妨多了解一些引用的知識(shí),在 Java 中有四種引用類型,分別為:
強(qiáng)引用(Strong Reference):如Object obj = new Object(),這類引用是 Java 程序中最普遍的。只要強(qiáng)引用還存在,垃圾收集器就永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。
軟引用(Soft Reference):它用來描述一些可能還有用,但并非必須的對(duì)象。在系統(tǒng)內(nèi)存不夠用時(shí),這類引用關(guān)聯(lián)的對(duì)象將被垃圾收集器回收。JDK1.2 之后提供了SoftReference類來實(shí)現(xiàn)軟引用。
弱引用(Weak Reference):它也是用來描述非必須對(duì)象的,但它的強(qiáng)度比軟引用更弱些,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時(shí),無論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。在 JDK1.2 之后,提供了WeakReference類來實(shí)現(xiàn)弱引用。
虛引用(Phantom Reference):也稱為幻引用,最弱的一種引用關(guān)系,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無法通過虛引用來取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的是希望能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。JDK1.2 之后提供了PhantomReference類來實(shí)現(xiàn)虛引用。
垃圾回收
通過上面的介紹,我們已經(jīng)知道了什么是垃圾以及如何判斷一個(gè)對(duì)象是否是垃圾。那么接下來,我們就來了解如何回收垃圾,這就是垃圾回收算法和垃圾回收器需要做的事情了。
垃圾回收算法
標(biāo)記-清除算法
標(biāo)記-清除(Tracing Collector)算法是最基礎(chǔ)的收集算法,為了解決引用計(jì)數(shù)法的問題而提出。它使用了根集的概念,它分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所需回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象,它的標(biāo)記過程其實(shí)就是前面的可達(dá)性分析法中判定垃圾對(duì)象的標(biāo)記過程。
優(yōu)點(diǎn):不需要進(jìn)行對(duì)象的移動(dòng),并且僅對(duì)不存活的對(duì)象進(jìn)行處理,在存活對(duì)象比較多的情況下極為高效。
缺點(diǎn):標(biāo)記和清除過程的效率都不高,這種方法需要使用一個(gè)空閑列表來記錄所有的空閑區(qū)域以及大小,對(duì)空閑列表的管理會(huì)增加分配對(duì)象時(shí)的工作量;標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,雖然空閑區(qū)域的大小是足夠的,但卻可能沒有一個(gè)單一區(qū)域能夠滿足這次分配所需的大小,因此本次分配還是會(huì)失敗,不得不觸發(fā)另一次垃圾收集動(dòng)作。
下圖為“標(biāo)記-清除”算法的示意圖:

下圖為使用“標(biāo)記-清除”算法回收前后的狀態(tài):

標(biāo)記-整理算法
標(biāo)記-整理(Compacting Collector)算法標(biāo)記的過程與“標(biāo)記-清除”算法中的標(biāo)記過程一樣,但對(duì)標(biāo)記后出的垃圾對(duì)象的處理情況有所不同,它不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。在基于“標(biāo)記-整理”算法的收集器的實(shí)現(xiàn)中,一般增加句柄和句柄表。
優(yōu)點(diǎn):經(jīng)過整理之后,新對(duì)象的分配只需要通過指針碰撞便能完成,比較簡(jiǎn)單;使用這種方法,空閑區(qū)域的位置是始終可知的,也不會(huì)再有碎片的問題了。
缺點(diǎn):GC 暫停的時(shí)間會(huì)增長(zhǎng),因?yàn)槟阈枰獙⑺械膶?duì)象都拷貝到一個(gè)新的地方,還得更新它們的引用地址。
下圖為“標(biāo)記-整理”算法的示意圖:

下圖為使用“標(biāo)記-整理”算法回收前后的狀態(tài):

復(fù)制算法
復(fù)制(Copying Collector)算法的提出是為了克服句柄的開銷和解決堆碎片的垃圾回收。它將內(nèi)存按容量分為大小相等的兩塊,每次只使用其中的一塊(對(duì)象面),當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊內(nèi)存上面(空閑面),然后再把已使用過的內(nèi)存空間一次清理掉。
復(fù)制算法比較適合于新生代(短生存期的對(duì)象),在老年代(長(zhǎng)生存期的對(duì)象)中,對(duì)象存活率比較高,如果執(zhí)行較多的復(fù)制操作,效率將會(huì)變低,所以老年代一般會(huì)選用其他算法,如“標(biāo)記-整理”算法。一種典型的基于復(fù)制算法的垃圾回收是stop-and-copy算法,它將堆分成對(duì)象區(qū)和空閑區(qū),在對(duì)象區(qū)與空閑區(qū)的切換過程中,程序暫停執(zhí)行。
優(yōu)點(diǎn):標(biāo)記階段和復(fù)制階段可以同時(shí)進(jìn)行;每次只對(duì)一塊內(nèi)存進(jìn)行回收,運(yùn)行高效;只需移動(dòng)棧頂指針,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單;內(nèi)存回收時(shí)不用考慮內(nèi)存碎片的出現(xiàn)。
缺點(diǎn):需要一塊能容納下所有存活對(duì)象的額外的內(nèi)存空間。因此,可一次性分配的最大內(nèi)存縮小了一半。
下圖為復(fù)制算法的示意圖:

下圖為使用復(fù)制算法回收前后的狀態(tài):

分代收集算法
分代收集(Generational Collector)算法的將堆內(nèi)存劃分為新生代、老年代和永久代。新生代又被進(jìn)一步劃分為 Eden 和 Survivor 區(qū),其中 Survivor 由 FromSpace(Survivor0)和 ToSpace(Survivor1)組成。所有通過new創(chuàng)建的對(duì)象的內(nèi)存都在堆中分配,其大小可以通過-Xmx和-Xms來控制。分代收集,是基于這樣一個(gè)事實(shí):不同的對(duì)象的生命周期是不一樣的。因此,可以將不同生命周期的對(duì)象分代,不同的代采取不同的回收算法進(jìn)行垃圾回收,以便提高回收效率。

新生代(Young Generation):幾乎所有新生成的對(duì)象首先都是放在年輕代的。新生代內(nèi)存按照 8:1:1 的比例分為一個(gè) Eden 區(qū)和兩個(gè) Survivor(Survivor0,Survivor1)區(qū)。大部分對(duì)象在 Eden 區(qū)中生成。當(dāng)新對(duì)象生成,Eden 空間申請(qǐng)失敗(因?yàn)榭臻g不足等),則會(huì)發(fā)起一次 GC(Scavenge GC)?;厥諘r(shí)先將 Eden 區(qū)存活對(duì)象復(fù)制到一個(gè) Survivor0 區(qū),然后清空 Eden 區(qū),當(dāng)這個(gè) Survivor0 區(qū)也存放滿了時(shí),則將 Eden 區(qū)和 Survivor0 區(qū)存活對(duì)象復(fù)制到另一個(gè) Survivor1 區(qū),然后清空 Eden 和這個(gè) Survivor0 區(qū),此時(shí) Survivor0 區(qū)是空的,然后將 Survivor0 區(qū)和 Survivor1 區(qū)交換,即保持 Survivor1 區(qū)為空, 如此往復(fù)。當(dāng) Survivor1 區(qū)不足以存放 Eden 和 Survivor0 的存活對(duì)象時(shí),就將存活對(duì)象直接存放到老年代。當(dāng)對(duì)象在 Survivor 區(qū)躲過一次 GC 的話,其對(duì)象年齡便會(huì)加 1,默認(rèn)情況下,如果對(duì)象年齡達(dá)到 15 歲,就會(huì)移動(dòng)到老年代中。若是老年代也滿了就會(huì)觸發(fā)一次 Full GC,也就是新生代、老年代都進(jìn)行回收。新生代大小可以由-Xmn來控制,也可以用-XX:SurvivorRatio來控制 Eden 和 Survivor 的比例。
老年代(Old Generation):在新生代中經(jīng)歷了 N 次垃圾回收后仍然存活的對(duì)象,就會(huì)被放到年老代中。因此,可以認(rèn)為年老代中存放的都是一些生命周期較長(zhǎng)的對(duì)象。內(nèi)存比新生代也大很多(大概比例是 1:2),當(dāng)老年代內(nèi)存滿時(shí)觸發(fā) Major GC 即 Full GC,F(xiàn)ull GC 發(fā)生頻率比較低,老年代對(duì)象存活時(shí)間比較長(zhǎng),存活率高。一般來說,大對(duì)象會(huì)被直接分配到老年代。所謂的大對(duì)象是指需要大量連續(xù)存儲(chǔ)空間的對(duì)象,最常見的一種大對(duì)象就是大數(shù)組。當(dāng)然分配的規(guī)則并不是百分之百固定的,這要取決于當(dāng)前使用的是哪種垃圾收集器組合和 JVM 的相關(guān)參數(shù)。
永久代(Permanent Generation):用于存放靜態(tài)文件(class類、方法)和常量等。永久代對(duì)垃圾回收沒有顯著影響,但是有些應(yīng)用可能動(dòng)態(tài)生成或者調(diào)用一些class,例如 Hibernate 等,在這種時(shí)候需要設(shè)置一個(gè)比較大的持久代空間來存放這些運(yùn)行過程中新增的類。對(duì)永久代的回收主要回收兩部分內(nèi)容:廢棄常量和無用的類。永久代在 Java SE8 特性中已經(jīng)被移除了,取而代之的是元空間(MetaSpace),因此也不會(huì)再出現(xiàn)java.lang.OutOfMemoryError: PermGen error的錯(cuò)誤了。
特別地,在分代收集算法中,對(duì)象的存儲(chǔ)具有以下特點(diǎn):
1、對(duì)象優(yōu)先在 Eden 區(qū)分配。
2、大對(duì)象直接進(jìn)入老年代。
3、長(zhǎng)期存活的對(duì)象將進(jìn)入老年代,默認(rèn)為 15 歲。
對(duì)于晉升老年代的分代年齡閾值,我們可以通過-XX:MaxTenuringThreshold參數(shù)進(jìn)行控制。在這里,不知道大家有沒有對(duì)這個(gè)默認(rèn)的 15 歲分代年齡產(chǎn)生過疑惑,為什么不是 16 或者 17 呢?實(shí)際上,HotSpot 虛擬機(jī)的對(duì)象頭其中一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼、GC 分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程 ID、偏向時(shí)間戳等,這部分?jǐn)?shù)據(jù)的長(zhǎng)度在 32 位和 64 位的虛擬機(jī)(未開啟壓縮指針)中分別為 32bit 和 64bit,官方稱它為Mark word。
例如,在 32 位的 HotSpot 虛擬機(jī)中,如果對(duì)象處于未被鎖定的狀態(tài)下,那么Mark Word的 32bit 空間中 25bit 用于存儲(chǔ)對(duì)象哈希碼,4bit 用于存儲(chǔ)對(duì)象分代年齡,2bit 用于存儲(chǔ)鎖標(biāo)志位,1bit 固定為 0,其中對(duì)象的分代年齡占 4 位,也就是從0000到1111,而其值最大為 15,所以分代年齡也就不可能超過 15 這個(gè)數(shù)值了。
除此之外,我們?cè)賮砗?jiǎn)單了解一下 GC 的分類:
新生代 GC(Minor GC / Scavenge GC):發(fā)生在新生代的垃圾收集動(dòng)作。因?yàn)?Java 對(duì)象大多都具有朝生夕滅的特性,因此 Minor GC 非常頻繁(不一定等 Eden 區(qū)滿了才觸發(fā)),一般回收速度也比較快。在新生代中,每次垃圾收集時(shí)都會(huì)發(fā)現(xiàn)有大量對(duì)象死去,只有少量存活,因此可選用復(fù)制算法來完成收集。
老年代 GC(Major GC / Full GC):發(fā)生在老年代的垃圾回收動(dòng)作。Major GC 經(jīng)常會(huì)伴隨至少一次 Minor GC。由于老年代中的對(duì)象生命周期比較長(zhǎng),因此 Major GC 并不頻繁,一般都是等待老年代滿了后才進(jìn)行 Full GC,而且其速度一般會(huì)比 Minor GC 慢10倍以上。另外,如果分配了 Direct Memory,在老年代中進(jìn)行 Full GC 時(shí),會(huì)順便清理掉 Direct Memory 中的廢棄對(duì)象。而老年代中因?yàn)閷?duì)象存活率高、沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記-清除”算法或“標(biāo)記-整理”算法來進(jìn)行回收。
新生代采用空閑指針的方式來控制 GC 觸發(fā),指針保持最后一個(gè)分配的對(duì)象在新生代區(qū)間的位置,當(dāng)有新的對(duì)象要分配內(nèi)存時(shí),用于檢查空間是否足夠,不夠就觸發(fā) GC。當(dāng)連續(xù)分配對(duì)象時(shí),對(duì)象會(huì)逐漸從 Eden 到 Survivor,最后到老年代。
再多說一句,在某些場(chǎng)景下,老年代的對(duì)象可能引用新生代的對(duì)象,那標(biāo)記存活對(duì)象的時(shí)候,需要掃描老年代中的所有對(duì)象。因?yàn)樵搶?duì)象擁有對(duì)新生代對(duì)象的引用,那么這個(gè)引用也會(huì)被稱為GC Roots。那是不是要做全堆掃描呢?成本也太高了吧?
HotSpot 給出的解決方案是一項(xiàng)叫做卡表(Card Table)的技術(shù),該技術(shù)將整個(gè)堆劃分為一個(gè)個(gè)大小為 512 字節(jié)的卡,并且維護(hù)一個(gè)卡表,用來存儲(chǔ)每張卡的一個(gè)標(biāo)識(shí)位。這個(gè)標(biāo)識(shí)位代表對(duì)應(yīng)的卡是否可能存有指向新生代對(duì)象的引用。如果可能存在,那么我們就認(rèn)為這張卡是臟的。
在進(jìn)行 Minor GC 的時(shí)候,我們便可以不用掃描整個(gè)老年代,而是在卡表中尋找臟卡,并將臟卡中的對(duì)象加入到 Minor GC 的GC Roots里。當(dāng)完成所有臟卡的掃描之后,Java 虛擬機(jī)便會(huì)將所有臟卡的標(biāo)識(shí)位清零。
想要保證每個(gè)可能有指向新生代對(duì)象引用的卡都被標(biāo)記為臟卡,那么 Java 虛擬機(jī)需要截獲每個(gè)引用型實(shí)例變量的寫操作,并作出對(duì)應(yīng)的寫標(biāo)識(shí)位操作。
卡表能用于減少老年代的全堆空間掃描,這能很大的提升 GC 效率。
垃圾回收器
垃圾回收(GC)線程與應(yīng)用線程保持相對(duì)獨(dú)立,當(dāng)系統(tǒng)需要執(zhí)行垃圾回收任務(wù)時(shí),先停止工作線程,然后命令 GC 線程工作。以串行模式工作的收集器,稱為Serial Collector,即串行收集器;與之相對(duì)的是以并行模式工作的收集器,稱為Paraller Collector,即并行收集器。
Serial 收集器
串行收集器采用單線程方式進(jìn)行收集,且在 GC 線程工作時(shí),系統(tǒng)不允許應(yīng)用線程打擾。此時(shí),應(yīng)用程序進(jìn)入暫停狀態(tài),即 Stop-the-world。Stop-the-world 暫停時(shí)間的長(zhǎng)短,是衡量一款收集器性能高低的重要指標(biāo)。Serial 是針對(duì)新生代的垃圾回收器,采用“復(fù)制”算法。
ParNew 收集器
并行收集器充分利用了多處理器的優(yōu)勢(shì),采用多個(gè) GC 線程并行收集??上攵?,多條 GC 線程執(zhí)行顯然比只使用一條 GC 線程執(zhí)行的效率更高。一般來說,與串行收集器相比,在多處理器環(huán)境下工作的并行收集器能夠極大地縮短 Stop-the-world 時(shí)間。ParNew 是針對(duì)新生代的垃圾回收器,采用“復(fù)制”算法,可以看成是 Serial 的多線程版本
Parallel Scavenge 收集器
Parallel Scavenge 是針對(duì)新生代的垃圾回收器,采用“復(fù)制”算法,和 ParNew 類似,但更注重吞吐率。在 ParNew 的基礎(chǔ)上演化而來的 Parallel Scanvenge 收集器被譽(yù)為“吞吐量?jī)?yōu)先”收集器。吞吐量就是 CPU 用于運(yùn)行用戶代碼的時(shí)間與 CPU 總消耗時(shí)間的比值,即吞吐量 = 運(yùn)行用戶代碼時(shí)間 /(運(yùn)行用戶代碼時(shí)間 + 垃圾收集時(shí)間)。如虛擬機(jī)總運(yùn)行了 100 分鐘,其中垃圾收集花掉 1 分鐘,那吞吐量就是99%。
Parallel Scanvenge 收集器在 ParNew 的基礎(chǔ)上提供了一組參數(shù),用于配置期望的收集時(shí)間或吞吐量,然后以此為目標(biāo)進(jìn)行收集。通過 VM 選項(xiàng)可以控制吞吐量的大致范圍:
-XX:MaxGCPauseMills:期望收集時(shí)間上限,用來控制收集對(duì)應(yīng)用程序停頓的影響。
-XX:GCTimeRatio:期望的 GC 時(shí)間占總時(shí)間的比例,用來控制吞吐量。
-XX:UseAdaptiveSizePolicy:自動(dòng)分代大小調(diào)節(jié)策略。
但要注意停頓時(shí)間與吞吐量這兩個(gè)目標(biāo)是相悖的,降低停頓時(shí)間的同時(shí)也會(huì)引起吞吐的降低。因此需要將目標(biāo)控制在一個(gè)合理的范圍中。
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本,單線程收集器,采用“標(biāo)記-整理”算法。這個(gè)收集器的主要意義也是在于給 Client 模式下的虛擬機(jī)使用。
Parallel Old 收集器
Parallel Old 是 Parallel Scanvenge 收集器的老年代版本,多線程收集器,采用“標(biāo)記-整理”算法。
CMS收集器
CMS(Concurrent Mark Swee)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。CMS 收集器僅作用于老年代的收集,采用“標(biāo)記-清除”算法,它的運(yùn)作過程分為 4 個(gè)步驟:
初始標(biāo)記(CMS initial mark)
并發(fā)標(biāo)記(CMS concurrent mark)
重新標(biāo)記(CMS remark)
并發(fā)清除(CMS concurrent sweep)
其中,初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍然需要 Stop-the-world。初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快,并發(fā)標(biāo)記階段就是進(jìn)行GC Roots Tracing的過程,而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間一般會(huì)比初始階段稍長(zhǎng)一些,但遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短。
CMS 以流水線方式拆分了收集周期,將耗時(shí)長(zhǎng)的操作單元保持與應(yīng)用線程并發(fā)執(zhí)行。只將那些必需 STW 才能執(zhí)行的操作單元單獨(dú)拎出來,控制這些單元在恰當(dāng)?shù)臅r(shí)機(jī)運(yùn)行,并能保證僅需短暫的時(shí)間就可以完成。這樣,在整個(gè)收集周期內(nèi),只有兩次短暫的暫停(初始標(biāo)記和重新標(biāo)記),達(dá)到了近似并發(fā)的目的。
CMS 收集器優(yōu)點(diǎn):并發(fā)收集,低停頓。
CMS 收集器缺點(diǎn):
CMS 收集器對(duì) CPU 資源非常敏感;
CMS 收集器無法處理浮動(dòng)垃圾;
CMS 收集器是基于“標(biāo)記-清除”算法,該算法的缺點(diǎn)都有。
CMS 收集器之所以能夠做到并發(fā),根本原因在于采用基于“標(biāo)記-清除”的算法并對(duì)算法過程進(jìn)行了細(xì)粒度的分解。前面已經(jīng)介紹過“標(biāo)記-清除”算法將產(chǎn)生大量的內(nèi)存碎片這對(duì)新生代來說是難以接受的,因此新生代的收集器并未提供 CMS 版本。
G1 收集器
G1(Garbage First)重新定義了堆空間,打破了原有的分代模型,將堆劃分為一個(gè)個(gè)區(qū)域。這么做的目的是在進(jìn)行收集時(shí)不必在全堆范圍內(nèi)進(jìn)行,這是它最顯著的特點(diǎn)。區(qū)域劃分的好處就是帶來了停頓時(shí)間可預(yù)測(cè)的收集模型:用戶可以指定收集操作在多長(zhǎng)時(shí)間內(nèi)完成,即 G1 提供了接近實(shí)時(shí)的收集特性。G1 與 CMS 的特征對(duì)比如下:

G1 具備如下特點(diǎn):
并行與并發(fā):G1 能充分利用多 CPU、多核環(huán)境下的硬件優(yōu)勢(shì),使用多個(gè) CPU 來縮短 Stop-the-world 停頓的時(shí)間,部分其他收集器原來需要停頓 Java 線程執(zhí)行的 GC 操作,G1 收集器仍然可以通過并發(fā)的方式讓 Java 程序繼續(xù)運(yùn)行。
分代收集:打破了原有的分代模型,將堆劃分為一個(gè)個(gè)區(qū)域。
空間整合:與 CMS 的“標(biāo)記-清除”算法不同,G1 從整體來看是基于“標(biāo)記-整理”算法實(shí)現(xiàn)的收集器,從局部(兩個(gè) Region 之間)上來看是基于“復(fù)制”算法實(shí)現(xiàn)的。但無論如何,這兩種算法都意味著 G1 運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片,收集后能提供規(guī)整的可用內(nèi)存。這種特性有利于程序長(zhǎng)時(shí)間運(yùn)行,分配大對(duì)象時(shí)不會(huì)因?yàn)闊o法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次 GC。
可預(yù)測(cè)的停頓:這是 G1 相對(duì)于 CMS 的一個(gè)優(yōu)勢(shì),降低停頓時(shí)間是 G1 和 CMS 共同的關(guān)注點(diǎn)。
在 G1 之前的其他收集器進(jìn)行收集的范圍都是整個(gè)新生代或者老年代,而 G1 不再是這樣。在堆的結(jié)構(gòu)設(shè)計(jì)時(shí),G1 打破了以往將收集范圍固定在新生代或老年代的模式,G1 將堆分成許多相同大小的區(qū)域單元,每個(gè)單元稱為 Region,Region 是一塊地址連續(xù)的內(nèi)存空間,G1 模塊的組成如下圖所示:

堆內(nèi)存會(huì)被切分成為很多個(gè)固定大小的 Region,每個(gè)是連續(xù)范圍的虛擬內(nèi)存。堆內(nèi)存中一個(gè) Region 的大小可以通過-XX:G1HeapRegionSize參數(shù)指定,其區(qū)間最小為 1M、最大為 32M,默認(rèn)把堆內(nèi)存按照 2048 份均分。
每個(gè) Region 被標(biāo)記了 E、S、O 和 H,這些區(qū)域在邏輯上被映射為 Eden,Survivor 和老年代。存活的對(duì)象從一個(gè)區(qū)域轉(zhuǎn)移(即復(fù)制或移動(dòng))到另一個(gè)區(qū)域,區(qū)域被設(shè)計(jì)為并行收集垃圾,可能會(huì)暫停所有應(yīng)用線程。
如上圖所示,區(qū)域可以分配到 Eden,Survivor 和老年代。此外,還有第四種類型,被稱為巨型區(qū)域(Humongous Region)。Humongous 區(qū)域是為了那些存儲(chǔ)超過 50% 標(biāo)準(zhǔn) Region 大小的對(duì)象而設(shè)計(jì)的,它用來專門存放巨型對(duì)象。如果一個(gè) H 區(qū)裝不下一個(gè)巨型對(duì)象,那么 G1 會(huì)尋找連續(xù)的 H 分區(qū)來存儲(chǔ)。為了能找到連續(xù)的 H 區(qū),有時(shí)候不得不啟動(dòng) Full GC。
G1 收集器之所以能建立可預(yù)測(cè)的停頓時(shí)間模型,是因?yàn)樗梢杂杏?jì)劃地避免在整個(gè) Java 堆中進(jìn)行全區(qū)域的垃圾收集。G1 會(huì)通過一個(gè)合理的計(jì)算模型,計(jì)算出每個(gè) Region 的收集成本并量化,這樣一來,收集器在給定了“停頓”時(shí)間限制的情況下,總是能選擇一組恰當(dāng)?shù)?Region 作為收集目標(biāo),讓其收集開銷滿足這個(gè)限制條件,以此達(dá)到實(shí)時(shí)收集的目的。
對(duì)于打算從 CMS 或者 ParallelOld 收集器遷移過來的應(yīng)用,按照官方的建議,如果發(fā)現(xiàn)符合如下特征,可以考慮更換成 G1 收集器以追求更佳性能:
實(shí)時(shí)數(shù)據(jù)占用了超過半數(shù)的堆空間;
對(duì)象分配率或“晉升”的速度變化明顯;
期望消除耗時(shí)較長(zhǎng)的GC或停頓(超過 0.5 ~ 1 秒)。
G1 收集的運(yùn)作過程大致如下:
初始標(biāo)記(Initial Marking):僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,并且修改 TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序并發(fā)運(yùn)行時(shí),能在正確可用的 Region 中創(chuàng)建新對(duì)象,這階段需要停頓線程,但耗時(shí)很短。
并發(fā)標(biāo)記(Concurrent Marking):是從GC Roots開始堆中對(duì)象進(jìn)行可達(dá)性分析,找出存活的對(duì)象,這階段耗時(shí)較長(zhǎng),但可與用戶程序并發(fā)執(zhí)行。
最終標(biāo)記(Final Marking):是為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分標(biāo)記記錄,虛擬機(jī)將這段時(shí)間對(duì)象變化記錄在線程 Remembered Set Logs 里面,最終標(biāo)記階段需要把 Remembered Set Logs 的數(shù)據(jù)合并到 Remembered Set 中,這階段需要停頓線程,但是可并行執(zhí)行。
篩選回收(Live Data Counting and Evacuation):首先對(duì)各個(gè) Region 的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的 GC 停頓時(shí)間來制定回收計(jì)劃。這個(gè)階段也可以做到與用戶程序一起并發(fā)執(zhí)行,但是因?yàn)橹换厥找徊糠?Region,時(shí)間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率。
G1 的 GC 模式可以分為兩種,分別為:
Young GC:在分配一般對(duì)象(非巨型對(duì)象)時(shí),當(dāng)所有 Eden 區(qū)域使用達(dá)到最大閥值并且無法申請(qǐng)足夠內(nèi)存時(shí),會(huì)觸發(fā)一次 YoungGC。每次 Young GC 會(huì)回收所有 Eden 以及 Survivor 區(qū),并且將存活對(duì)象復(fù)制到 Old 區(qū)以及另一部分的 Survivor 區(qū)。
Mixed GC:當(dāng)越來越多的對(duì)象晉升到老年代時(shí),為了避免堆內(nèi)存被耗盡,虛擬機(jī)會(huì)觸發(fā)一個(gè)混合的垃圾收集器,即 Mixed GC,該算法并不是一個(gè) Old GC,除了回收整個(gè)新生代,還會(huì)回收一部分的老年代,這里需要注意:是一部分老年代,而不是全部老年代,可以選擇哪些 Old 區(qū)域進(jìn)行收集,從而可以對(duì)垃圾回收的耗時(shí)時(shí)間進(jìn)行控制。G1 沒有 Full GC概念,需要 Full GC 時(shí),調(diào)用 Serial Old GC 進(jìn)行全堆掃描。
查看 JVM 使用的默認(rèn)垃圾收集器
在 Mac 終端或者 Windows 的 CMD 執(zhí)行如下命令:
java -XX:+PrintCommandLineFlags -version
以我的電腦為例,執(zhí)行結(jié)果為:

在此,給出垃圾收集相關(guān)的常用參數(shù)及其含義:

由此可知,JDK 8 默認(rèn)打開了UseParallelGC參數(shù),因此使用了Parallel Scavenge + Serial Old的收集器組合進(jìn)行內(nèi)存回收。
到這里,關(guān)于 JVM 垃圾回收機(jī)制及其實(shí)現(xiàn)原理,我們就講完了,希望能夠?qū)Υ蠹矣兴鶐椭?/span>
————————————————
版權(quán)聲明:本文為CSDN博主「CG國(guó)斌」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。
原文鏈接:
https://blog.csdn.net/qq_35246620/article/details/80522720
粉絲福利:Java從入門到入土學(xué)習(xí)路線圖
??????

??長(zhǎng)按上方微信二維碼 2 秒
感謝點(diǎn)贊支持下哈 
