一、為什么要進(jìn)行垃圾回收
隨著程序的運(yùn)行,內(nèi)存中存在的實例對象、變量等信息占據(jù)的內(nèi)存越來越多,其中有很多對象再也用不到,這些用不到的對象就被稱之為垃圾,如果不及時進(jìn)行垃圾回收,必然會帶來程序性能的下降,甚至?xí)驗榭捎脙?nèi)存不足造成一些不必要的系統(tǒng)異常。
垃圾回收機(jī)制主要是對 JVM 中堆內(nèi)存進(jìn)行管理,如果對 JVM 相關(guān)的概念還不了解,可以看一看《JVM 從入門到出門》這篇文章。
二、如何判定對象是否為垃圾
1、引用計數(shù)法
給對象添加一引用計數(shù)器,被引用一次計數(shù)器值就加 1;當(dāng)引用失效時,計數(shù)器值就減 1;計數(shù)器為 0 時,對象就是垃圾。
優(yōu)點(diǎn)是執(zhí)行效率高,缺點(diǎn)是無法解決對象之間相互循環(huán)引用的問題。
2、可達(dá)性分析算法
以 GC Roots 為起始點(diǎn)進(jìn)行搜索,判斷對象的引用鏈?zhǔn)欠窨蛇_(dá),可達(dá)的對象都是存活的,不可達(dá)的對象可被回收。GC Roots 一般包含以下內(nèi)容:
- 虛擬機(jī)棧中局部變量表中引用的對象
- 本地方法棧中 JNI 中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中的常量引用的對象
對象死亡(被回收)前的最后一次掙扎:
即使在可達(dá)性分析算法中不可達(dá)的對象,也并非是“必死不可”,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經(jīng)歷兩次標(biāo)記過程。
第一次標(biāo)記:如果對象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒有與 GC Roots 相連接的引用鏈,那它將會被第一次標(biāo)記;
第二次標(biāo)記:在第一次標(biāo)記后接著會進(jìn)行一次篩選,篩選的條件是此對象是否有必要執(zhí)行finalize()方法。在finalize()方法中沒有重新與引用鏈建立關(guān)聯(lián)關(guān)系的,將被進(jìn)行第二次標(biāo)記。
第二次標(biāo)記成功的對象將真的會被回收,如果對象在finalize()方法中重新與引用鏈建立了關(guān)聯(lián)關(guān)系,那么將會逃離本次回收,繼續(xù)存活。
三、回收垃圾的算法
回收垃圾的算法主要有 4 種:標(biāo)記清除算法, 標(biāo)記整理算法,復(fù)制算法,分代收集算法。下面分別介紹。
1、標(biāo)記清除
標(biāo)記:從 GC Roots 為起始點(diǎn)進(jìn)行掃描,如果是活動對象,則程序會在對象頭部打上標(biāo)記。
清除:對堆內(nèi)存從頭到尾進(jìn)行線性遍歷,回收不可達(dá)對象。

但是,標(biāo)記清除算法會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,導(dǎo)致無法給大對象分配內(nèi)存。例如上圖中 B 與 E 之間只剩 2 格,若有一個新對象要占用 3 格,則需要開辟另外的內(nèi)存或者 Full GC。
2、標(biāo)記整理
標(biāo)記:從 GC Roots 為起始點(diǎn)進(jìn)行掃描,如果是活動對象,則程序會在對象頭部打上標(biāo)記。
整理:移動所有存活對象,且按照內(nèi)存地址次序依次排列,然后將末端以后的內(nèi)存地址全部回收。

彌補(bǔ)了標(biāo)記清除算法的不足,不會產(chǎn)生內(nèi)存碎片。但是需要移動大量對象,處理效率比較低。
3、復(fù)制算法
將內(nèi)存劃分為大小相等的兩塊,每次只使用其中一塊,當(dāng)這一塊內(nèi)存用完了就將還存活的對象復(fù)制到另一塊上面,然后再把使用過的內(nèi)存空間進(jìn)行一次清理。

不會產(chǎn)生內(nèi)存碎片問題,順序分配內(nèi)存,執(zhí)行效率高,但每次只使用了一半的內(nèi)存,未免有點(diǎn)浪費(fèi)。
4、分代收集
分代收集實際上就是將上述 3 種算法綜合起來,針對不同的區(qū)域,采用不同的方法,按照對象的生命周期的不同劃分區(qū)域,采用不同的垃圾回收算法,以提高 JVM 回收效率。
Java 堆分為兩部分,Java 堆 = 新生代 + 老年代,默認(rèn)分別占堆空間為 1/3、2/3;其中,新生代 = Eden + From Survivor + To Survivor,默認(rèn)為 8:1:1。這樣劃分是由于對象生存周期的特殊性,針對不同的對象,采用不同的方法。

新生代使用:復(fù)制算法
老年代使用:標(biāo)記清除 或 標(biāo)記整理 算法
所有的對象都在 Eden 區(qū)創(chuàng)建,由于大部分對象都是“朝生夕滅”,只有少量對象能存活下來,所以在新生代采用復(fù)制算法,只有少量對象需要復(fù)制,這樣最劃算。
當(dāng) Eden 區(qū)滿了,那么就會觸發(fā)一次 Young GC,也就是年輕代垃圾回收。少量有用的對象會復(fù)制到 From 區(qū)。這樣整個Eden區(qū)就被清理干凈了,可以繼續(xù)創(chuàng)建新的對象。
當(dāng) Eden 區(qū)再次被用完,就再觸發(fā)一次 YoungGC,這個時候跟剛才稍稍有點(diǎn)區(qū)別。這次觸發(fā) Young GC 后,會將 Eden 區(qū)與 From 區(qū)還在被使用的對象復(fù)制到 To 區(qū),再下一次 YoungGC 的時候,則是將 Eden 區(qū)與 To 區(qū)中的還在被使用的對象復(fù)制到 From 區(qū)。
經(jīng)過若干次 YoungGC 后,有些對象在 From 與 To 之間來回游蕩,這時候 From 區(qū)與 To 區(qū)亮出了底線(閾值),這些家伙要是到現(xiàn)在還沒掛掉,對不起,一起復(fù)制老年代吧。
而在老年代,大部分對象任然會繼續(xù)存活下來,此時采用標(biāo)記整理或者標(biāo)記清除算法,這樣最劃算。
對象如何晉升到老年代?
1、經(jīng)歷一定次數(shù)的 Minor GC 任然存活的對象,默認(rèn) 15 次;
2、Eden 區(qū)或 Survivor 區(qū)域存放不下的對象;
3、新生成的大對象,直接放入老年代。
四、常見的垃圾收集器
Serial 垃圾收集器(單線程,復(fù)制算法):
Serial 是單線程收集,進(jìn)行垃圾收集時必須暫停所有工作線程。但是它簡單高效,JVM Client 模式下默認(rèn)的年輕代收集器。

ParNew 垃圾收集器(多線程,復(fù)制算法):
ParNew 是多線程收集器,是 CMS 默認(rèn)的新生代垃圾回收器,其他行為特點(diǎn)與 Serial 一樣。
Parallel Scavenge 垃圾收集器(多線程,復(fù)制算法):
Parallel Scavenge 和 ParNew 一樣,都是多線程、新生代垃圾收集器。兩者的區(qū)別在于:
Parallel Scavenge 追求 CPU 吞吐量,能夠在較短時間內(nèi)完成指定任務(wù),因此適合沒有交互的后臺計算;
ParNew 追求降低用戶停頓時間,適合交互式應(yīng)用。
Serial Old 垃圾收集器(單線程,標(biāo)記整理算法):
Serial Old 收集器是 Serial 的老年代版本,都是單線程收集器,都適合客戶端應(yīng)用。它們唯一的區(qū)別就是:Serial Old 工作在老年代,使用“標(biāo)記-整理”算法;Serial 工作在新生代,使用“復(fù)制”算法。
CMS 垃圾收集器(標(biāo)記清楚算法):
CMS (Concurrent Mark Sweep,并發(fā)標(biāo)記清除) 收集器是以獲取最短回收停頓時間為目標(biāo)的收集器(追求低停頓),它在垃圾收集時,用戶線程 和 GC 線程并發(fā)執(zhí)行,因此在垃圾收集過程中不會感到明顯的卡頓。
具體執(zhí)行過程如下圖:初始標(biāo)記 (Initial Mark) —> 并發(fā)標(biāo)記 (Concurrent Mark) —> 重新標(biāo)記 (Remark) —> 并發(fā)清除 (Concurrnet Sweep)。

初始標(biāo)記 (Initial Mark):僅僅只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對象,速度很快,需要 Stop The World。
并發(fā)標(biāo)記 (Concurrent Mark):從 GC Roots 的直接關(guān)聯(lián)對象開始遍歷整個對象圖的過程,耗時較長,但不需要停頓用戶線程,可與垃圾收集器線程一起并發(fā)執(zhí)行。
重新標(biāo)記 (Remark):該階段是為了修正并發(fā)標(biāo)記期間,因用戶程序運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄,這個階段需要 Stop The World,而且停頓時間通常比初始階段稍長一些,但也遠(yuǎn)比并發(fā)標(biāo)記階段的時間短。
并發(fā)清除 (Concurrnet Sweep):清理刪除掉標(biāo)記階段判斷已經(jīng)死亡的對象,由于不需要移動存活對象,所有這個階段可以和用戶線程并發(fā)執(zhí)行。
CMS 收集器是并發(fā)收集,有兩次 Stop The Words,兩次標(biāo)記,因為 GC 線程和應(yīng)用線程同時執(zhí)行,好比你媽在打掃房間,你還在扔紙屑,可能產(chǎn)生新的引用關(guān)系。
CMS 的缺點(diǎn):吞吐量低,無法處理浮動垃圾,導(dǎo)致頻繁 Full GC,使用“標(biāo)記-清除”算法產(chǎn)生碎片空間。
G1 垃圾收集器
G1 (Garbage-First) 是一款面向服務(wù)端應(yīng)用的垃圾收集器,它弱化了新生代和老年代的概念,雖然還保留了新生代和來年代的概念,但新生代和老年代不再是物理隔離的了,而是將堆劃分為一塊塊獨(dú)立的 Region(默認(rèn)將整堆劃分為 2048 個 Region),每個 Region 也不會確定地為某個代服務(wù),可以按需在年輕代和老年代之間切換。

內(nèi)存的回收是以 Region 為基本單位的,當(dāng)要進(jìn)行垃圾收集時,首先估計每個 Region 中垃圾的數(shù)量,每次都從垃圾回收價值最大的 Region 開始回收,因此可以獲得最大的回收效率。
什么是回收價值最大?
價值最大就是回收耗時最短,回收時間就是復(fù)制的時間,存活對象越多,回收時要復(fù)制的間就越長,回收的效益就越低,例如同等大小的兩個 Region,回收一個需要 100ms,另一個需要 50ms,那么 G1 肯定優(yōu)先回收 50ms 的,這樣就保證了在有效時間內(nèi)能回收更多的堆空間。
每個 Region 都有一個 Remembered Set,用來記錄該 Region 對象的引用對象所在的 Region。通過使用 Remembered Set,在做可達(dá)性分析的時候就可以避免全堆掃描。


從整體上看, G1 是基于“標(biāo)記-整理”算法實現(xiàn)的收集器,從局部(兩個 Region 之間)上看是基于“復(fù)制”算法實現(xiàn)的,這意味著運(yùn)行期間不會產(chǎn)生內(nèi)存空間碎片。