深入理解 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 來(lái)說(shuō),我們都不陌生,其是 Java Virtual Machine(Java 虛擬機(jī))的縮寫,它也是一個(gè)虛構(gòu)出來(lái)的計(jì)算機(jī),是通過(guò)在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來(lái)實(shí)現(xiàn)的。JVM 有自己完善的硬件架構(gòu),如處理器、堆棧等,還具有相應(yīng)的指令系統(tǒng),其本質(zhì)上就是一個(gè)程序,當(dāng)它在命令行上啟動(dòng)的時(shí)候,就開始執(zhí)行保存在某字節(jié)碼文件中的指令。
Java 語(yǔ)言的可移植性就是建立在 JVM 的基礎(chǔ)之上的,任何平臺(tái)只要裝有針對(duì)于該平臺(tái)的 Java 虛擬機(jī),字節(jié)碼文件(.class)就可以在該平臺(tái)上運(yùn)行,這就是“一次編譯,多次運(yùn)行”。除此之外,作為 Java 語(yǔ)言最重要的特性之一的自動(dòng)垃圾回收機(jī)制,也是基于 JVM 實(shí)現(xiàn)的。那么,自動(dòng)垃圾回收機(jī)制到底是如何實(shí)現(xiàn)的呢?在本文中,就讓我們一探究竟。
垃圾
什么是垃圾?
在 JVM 進(jìn)行垃圾回收之前,首先就是判斷哪些對(duì)象是垃圾,也就是說(shuō),要判斷哪些對(duì)象是可以被銷毀的,其占有的空間是可以被回收的。根據(jù) JVM 的架構(gòu)劃分,我們知道, 在 Java 世界中,幾乎所有的對(duì)象實(shí)例都在堆中存放,所以垃圾回收也主要是針對(duì)堆來(lái)進(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è)引用超過(guò)了生命周期(出作用域后)或者被設(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)起來(lái)比較簡(jiǎn)單,對(duì)程序不被長(zhǎng)時(shí)間打斷的實(shí)時(shí)環(huán)境比較有利。
缺點(diǎn):需要額外的空間來(lái)存儲(chǔ)計(jì)數(shù)器,難以檢測(cè)出對(duì)象之間的循環(huán)引用。
可達(dá)性分析法
可達(dá)性分析法也被稱之為根搜索法,可達(dá)性是指,如果一個(gè)對(duì)象會(huì)被至少一個(gè)在程序中的變量通過(guò)直接或間接的方式被其他可達(dá)的對(duì)象引用,則稱該對(duì)象就是可達(dá)的。更準(zhǔn)確的說(shuō),一個(gè)對(duì)象只有滿足下述兩個(gè)條件之一,就會(huì)被判斷為可達(dá)的:
對(duì)象是屬于根集中的對(duì)象
對(duì)象被一個(gè)可達(dá)的對(duì)象引用
在這里,我們引出了一個(gè)專有名詞,即根集,其是指正在執(zhí)行的 Java 程序可以訪問(wèn)的引用變量(注意,不是對(duì)象)的集合,程序可以使用引用變量訪問(wèn)對(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)開始向下搜索,搜索所走過(guò)的路徑稱為引用鏈,如果一個(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ì)象圖一直在變化的話是無(wú)法真正去遍歷它的。暫停應(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)行選定的。“長(zhǎng)時(shí)間執(zhí)行”的最明顯特征就是指令序列復(fù)用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等,所以具有這些功能的指令才會(huì)產(chǎn)生安全點(diǎn)。對(duì)于安全點(diǎn),另一個(gè)需要考慮的問(wèn)題就是如何在 GC 發(fā)生時(shí)讓所有線程(這里不包括執(zhí)行 JNI 調(diào)用的線程)都“跑”到最近的安全點(diǎn)上再停頓下來(lái)。兩種解決方案:
搶先式中斷(Preemptive Suspension):搶先式中斷不需要線程的執(zhí)行代碼主動(dòng)去配合,在 GC 發(fā)生時(shí),首先把所有線程全部中斷,如果發(fā)現(xiàn)有線程中斷的地方不在安全點(diǎn)上,就恢復(fù)線程,讓它“跑”到安全點(diǎn)上。現(xiàn)在幾乎沒有虛擬機(jī)采用這種方式來(lái)暫停線程從而響應(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)記過(guò)程:
如果對(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)用過(guò),虛擬機(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)引用的問(wèn)題,不需要占用額外的空間
缺點(diǎn):多線程場(chǎng)景下,其他線程可能會(huì)更新已經(jīng)訪問(wèn)過(guò)的對(duì)象的引用
在上面的介紹中,我們多次提到了“引用”這個(gè)概念,在此我們不妨多了解一些引用的知識(shí),在 Java 中有四種引用類型,分別為:
強(qiáng)引用(Strong Reference):如Object obj = new Object(),這類引用是 Java 程序中最普遍的。只要強(qiáng)引用還存在,垃圾收集器就永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。
軟引用(Soft Reference):它用來(lái)描述一些可能還有用,但并非必須的對(duì)象。在系統(tǒng)內(nèi)存不夠用時(shí),這類引用關(guān)聯(lián)的對(duì)象將被垃圾收集器回收。JDK1.2 之后提供了SoftReference類來(lái)實(shí)現(xiàn)軟引用。
弱引用(Weak Reference):它也是用來(lái)描述非必須對(duì)象的,但它的強(qiáng)度比軟引用更弱些,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時(shí),無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。在 JDK1.2 之后,提供了WeakReference類來(lái)實(shí)現(xiàn)弱引用。
虛引用(Phantom Reference):也稱為幻引用,最弱的一種引用關(guān)系,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的是希望能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。JDK1.2 之后提供了PhantomReference類來(lái)實(shí)現(xiàn)虛引用。
垃圾回收
通過(guò)上面的介紹,我們已經(jīng)知道了什么是垃圾以及如何判斷一個(gè)對(duì)象是否是垃圾。那么接下來(lái),我們就來(lái)了解如何回收垃圾,這就是垃圾回收算法和垃圾回收器需要做的事情了。
垃圾回收算法
標(biāo)記-清除算法
標(biāo)記-清除(Tracing Collector)算法是最基礎(chǔ)的收集算法,為了解決引用計(jì)數(shù)法的問(wèn)題而提出。它使用了根集的概念,它分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所需回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象,它的標(biāo)記過(guò)程其實(shí)就是前面的可達(dá)性分析法中判定垃圾對(duì)象的標(biāo)記過(guò)程。
優(yōu)點(diǎn):不需要進(jìn)行對(duì)象的移動(dòng),并且僅對(duì)不存活的對(duì)象進(jìn)行處理,在存活對(duì)象比較多的情況下極為高效。
缺點(diǎn):標(biāo)記和清除過(guò)程的效率都不高,這種方法需要使用一個(gè)空閑列表來(lái)記錄所有的空閑區(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)記的過(guò)程與“標(biāo)記-清除”算法中的標(biāo)記過(guò)程一樣,但對(duì)標(biāo)記后出的垃圾對(duì)象的處理情況有所不同,它不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。在基于“標(biāo)記-整理”算法的收集器的實(shí)現(xiàn)中,一般增加句柄和句柄表。
優(yōu)點(diǎn):經(jīng)過(guò)整理之后,新對(duì)象的分配只需要通過(guò)指針碰撞便能完成,比較簡(jiǎn)單;使用這種方法,空閑區(qū)域的位置是始終可知的,也不會(huì)再有碎片的問(wèn)題了。
缺點(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)存上面(空閑面),然后再把已使用過(guò)的內(nèi)存空間一次清理掉。
復(fù)制算法比較適合于新生代(短生存期的對(duì)象),在老年代(長(zhǎng)生存期的對(duì)象)中,對(duì)象存活率比較高,如果執(zhí)行較多的復(fù)制操作,效率將會(huì)變低,所以老年代一般會(huì)選用其他算法,如“標(biāo)記-整理”算法。一種典型的基于復(fù)制算法的垃圾回收是stop-and-copy算法,它將堆分成對(duì)象區(qū)和空閑區(qū),在對(duì)象區(qū)與空閑區(qū)的切換過(guò)程中,程序暫停執(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)組成。所有通過(guò)new創(chuàng)建的對(duì)象的內(nèi)存都在堆中分配,其大小可以通過(guò)-Xmx和-Xms來(lái)控制。分代收集,是基于這樣一個(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)。回收時(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ū)躲過(guò)一次 GC 的話,其對(duì)象年齡便會(huì)加 1,默認(rèn)情況下,如果對(duì)象年齡達(dá)到 15 歲,就會(huì)移動(dòng)到老年代中。若是老年代也滿了就會(huì)觸發(fā)一次 Full GC,也就是新生代、老年代都進(jìn)行回收。新生代大小可以由-Xmn來(lái)控制,也可以用-XX:SurvivorRatio來(lái)控制 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),存活率高。一般來(lái)說(shuō),大對(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è)比較大的持久代空間來(lái)存放這些運(yùn)行過(guò)程中新增的類。對(duì)永久代的回收主要回收兩部分內(nèi)容:廢棄常量和無(wú)用的類。永久代在 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ì)于晉升老年代的分代年齡閾值,我們可以通過(guò)-XX:MaxTenuringThreshold參數(shù)進(jìn)行控制。在這里,不知道大家有沒有對(duì)這個(gè)默認(rèn)的 15 歲分代年齡產(chǎn)生過(guò)疑惑,為什么不是 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,所以分代年齡也就不可能超過(guò) 15 這個(gè)數(shù)值了。
除此之外,我們?cè)賮?lái)簡(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ù)制算法來(lái)完成收集。
老年代 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)記-整理”算法來(lái)進(jìn)行回收。
新生代采用空閑指針的方式來(lái)控制 GC 觸發(fā),指針保持最后一個(gè)分配的對(duì)象在新生代區(qū)間的位置,當(dāng)有新的對(duì)象要分配內(nèi)存時(shí),用于檢查空間是否足夠,不夠就觸發(fā) GC。當(dāng)連續(xù)分配對(duì)象時(shí),對(duì)象會(huì)逐漸從 Eden 到 Survivor,最后到老年代。
再多說(shuō)一句,在某些場(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è)卡表,用來(lái)存儲(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í)行的效率更高。一般來(lái)說(shuō),與串行收集器相比,在多處理器環(huán)境下工作的并行收集器能夠極大地縮短 Stop-the-world 時(shí)間。ParNew 是針對(duì)新生代的垃圾回收器,采用“復(fù)制”算法,可以看成是 Serial 的多線程版本
Parallel Scavenge 收集器
Parallel Scavenge 是針對(duì)新生代的垃圾回收器,采用“復(fù)制”算法,和 ParNew 類似,但更注重吞吐率。在 ParNew 的基礎(chǔ)上演化而來(lái)的 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)行收集。通過(guò) VM 選項(xiàng)可以控制吞吐量的大致范圍:
-XX:MaxGCPauseMills:期望收集時(shí)間上限,用來(lái)控制收集對(duì)應(yīng)用程序停頓的影響。
-XX:GCTimeRatio:期望的 GC 時(shí)間占總時(shí)間的比例,用來(lái)控制吞吐量。
-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)作過(guò)程分為 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的過(guò)程,而重新標(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ú)拎出來(lái),控制這些單元在恰當(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 收集器無(wú)法處理浮動(dòng)垃圾;
CMS 收集器是基于“標(biāo)記-清除”算法,該算法的缺點(diǎn)都有。
CMS 收集器之所以能夠做到并發(fā),根本原因在于采用基于“標(biāo)記-清除”的算法并對(duì)算法過(guò)程進(jìn)行了細(xì)粒度的分解。前面已經(jīng)介紹過(guò)“標(biāo)記-清除”算法將產(chǎn)生大量的內(nèi)存碎片這對(duì)新生代來(lái)說(shuō)是難以接受的,因此新生代的收集器并未提供 CMS 版本。
G1 收集器
G1(Garbage First)重新定義了堆空間,打破了原有的分代模型,將堆劃分為一個(gè)個(gè)區(qū)域。這么做的目的是在進(jìn)行收集時(shí)不必在全堆范圍內(nèi)進(jìn)行,這是它最顯著的特點(diǎn)。區(qū)域劃分的好處就是帶來(lái)了停頓時(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 來(lái)縮短 Stop-the-world 停頓的時(shí)間,部分其他收集器原來(lái)需要停頓 Java 線程執(zhí)行的 GC 操作,G1 收集器仍然可以通過(guò)并發(fā)的方式讓 Java 程序繼續(xù)運(yùn)行。
分代收集:打破了原有的分代模型,將堆劃分為一個(gè)個(gè)區(qū)域。
空間整合:與 CMS 的“標(biāo)記-清除”算法不同,G1 從整體來(lái)看是基于“標(biāo)記-整理”算法實(shí)現(xiàn)的收集器,從局部(兩個(gè) Region 之間)上來(lái)看是基于“復(fù)制”算法實(shí)現(xiàn)的。但無(wú)論如何,這兩種算法都意味著 G1 運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片,收集后能提供規(guī)整的可用內(nèi)存。這種特性有利于程序長(zhǎng)時(shí)間運(yùn)行,分配大對(duì)象時(shí)不會(huì)因?yàn)闊o(wú)法找到連續(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 的大小可以通過(guò)-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ǔ)超過(guò) 50% 標(biāo)準(zhǔn) Region 大小的對(duì)象而設(shè)計(jì)的,它用來(lái)專門存放巨型對(duì)象。如果一個(gè) H 區(qū)裝不下一個(gè)巨型對(duì)象,那么 G1 會(huì)尋找連續(xù)的 H 分區(qū)來(lái)存儲(chǔ)。為了能找到連續(xù)的 H 區(qū),有時(shí)候不得不啟動(dòng) Full GC。
G1 收集器之所以能建立可預(yù)測(cè)的停頓時(shí)間模型,是因?yàn)樗梢杂杏?jì)劃地避免在整個(gè) Java 堆中進(jìn)行全區(qū)域的垃圾收集。G1 會(huì)通過(guò)一個(gè)合理的計(jì)算模型,計(jì)算出每個(gè) Region 的收集成本并量化,這樣一來(lái),收集器在給定了“停頓”時(shí)間限制的情況下,總是能選擇一組恰當(dāng)?shù)?Region 作為收集目標(biāo),讓其收集開銷滿足這個(gè)限制條件,以此達(dá)到實(shí)時(shí)收集的目的。
對(duì)于打算從 CMS 或者 ParallelOld 收集器遷移過(guò)來(lái)的應(yīng)用,按照官方的建議,如果發(fā)現(xiàn)符合如下特征,可以考慮更換成 G1 收集器以追求更佳性能:
實(shí)時(shí)數(shù)據(jù)占用了超過(guò)半數(shù)的堆空間;
對(duì)象分配率或“晉升”的速度變化明顯;
期望消除耗時(shí)較長(zhǎng)的GC或停頓(超過(guò) 0.5 ~ 1 秒)。
G1 收集的運(yùn)作過(guò)程大致如下:
初始標(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í)間來(lái)制定回收計(jì)劃。這個(gè)階段也可以做到與用戶程序一起并發(fā)執(zhí)行,但是因?yàn)橹换厥找徊糠?Region,時(shí)間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率。
G1 的 GC 模式可以分為兩種,分別為:
Young GC:在分配一般對(duì)象(非巨型對(duì)象)時(shí),當(dāng)所有 Eden 區(qū)域使用達(dá)到最大閥值并且無(wú)法申請(qǐng)足夠內(nèi)存時(shí),會(huì)觸發(fā)一次 YoungGC。每次 Young GC 會(huì)回收所有 Eden 以及 Survivor 區(qū),并且將存活對(duì)象復(fù)制到 Old 區(qū)以及另一部分的 Survivor 區(qū)。
Mixed GC:當(dāng)越來(lái)越多的對(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)贊支持下哈 
