前言
談起JVM, 那么就不得不提垃圾收集(Garbage Collection 通常被稱為“GC”).
什么是垃圾收集呢?
想解答這個(gè)問(wèn)題, 我們最好將問(wèn)題拆解開(kāi)
- 如何確定垃圾?
- 如何回收垃圾?
- 何時(shí)回收垃圾?
下面圍繞這三件事, 我們站在JVM層面梳理下垃圾收集的機(jī)制.
如何確定垃圾?
從JVM層面來(lái)看, 它管理的是生命周期內(nèi)的全部實(shí)例對(duì)象, 那么所謂的垃圾其實(shí)就是“無(wú)用的對(duì)象”.
那么它是如何確定“無(wú)用對(duì)象”的?
引用計(jì)數(shù)法
在 Java 中,引用和對(duì)象是有關(guān)聯(lián)的, 必須使用引用來(lái)操作對(duì)象.
People zs = new People();
zs.setName("張三");
zs是個(gè)引用, 和真正的對(duì)象“new People()”關(guān)聯(lián)
因此, 簡(jiǎn)單的辦法是通過(guò)引用計(jì)數(shù)來(lái)判斷一個(gè)對(duì)象是否可以回收.
所以在JVM中, 每個(gè)對(duì)象都在對(duì)象頭結(jié)構(gòu)中維護(hù)了一個(gè)引用計(jì)數(shù)屬性
- 對(duì)象被引用一次時(shí), 計(jì)數(shù)就加1
- 對(duì)象的引用被釋放時(shí),計(jì)數(shù)就減1
- 對(duì)象的計(jì)數(shù)為0的時(shí), 這個(gè)對(duì)象就可以被回收了
引用計(jì)數(shù)法聽(tīng)著雖然簡(jiǎn)單易懂, 判定效率也高.
但是,當(dāng)前主流的虛擬機(jī)都沒(méi)有采用這個(gè)算法來(lái)管理內(nèi)存,其中最主要的原因是它很難解決對(duì)象之間互相循環(huán)引用的問(wèn)題.
循環(huán)引用問(wèn)題
所謂對(duì)象之間互相循環(huán)引用,如下面代碼所示:
除了對(duì)象 objA 和 objB 相互引用著對(duì)方之外,這兩個(gè)對(duì)象之間再無(wú)任何引用.
但是它們因?yàn)榛ハ嘁脤?duì)方,導(dǎo)致它們的引用計(jì)數(shù)器都不為 0,于是引用計(jì)數(shù)算法無(wú)法通知 GC 回收器回收他們.
PS: 實(shí)際上以下示例是能回收的, 因?yàn)镴VM沒(méi)有采用引用計(jì)數(shù)法
public class ReferenceCountingGc {
public Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc obj1 = new ReferenceCountingGc();
ReferenceCountingGc obj2 = new ReferenceCountingGc();
obj1.instance = obj2;
obj2.instance = obj1;
obj1 = null;
obj2 = null;
}
}
可達(dá)性分析
為了解決引用計(jì)數(shù)法的循環(huán)引用問(wèn)題, Java 使用了可達(dá)性分析的方法.
可達(dá)性分析就是通過(guò)一系列的稱為 “GC Roots”的對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,節(jié)點(diǎn)所走過(guò)的路徑稱為引用鏈.
當(dāng)一個(gè)對(duì)象到 GC Roots 沒(méi)有任何引用鏈相連的話,則證明此對(duì)象是不可用的.
如下圖中的 Object 6 ~ Object 10 之間雖有引用關(guān)系,但它們到 GC Roots 不可達(dá), 因此為需要被回收的對(duì)象.

在Java中, GC Roots包括:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
- 本地方法棧(Native 方法)中引用的對(duì)象
- 方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
- 所有被同步鎖持有的對(duì)象
要注意的是:
- 不可達(dá)對(duì)象不等價(jià)于可回收對(duì)象
- 不可達(dá)對(duì)象變?yōu)榭苫厥諏?duì)象至少要經(jīng)過(guò)兩次標(biāo)記過(guò)程,兩次標(biāo)記后仍然是可回收對(duì)象,則將面臨回收
如何回收垃圾?
確定垃圾, 那么如何回收呢?
這就不得不談一系列的垃圾回收算法, 算法實(shí)現(xiàn)會(huì)因各個(gè)平臺(tái)虛擬機(jī)的差異而不同, 這里我們只談幾種主流的算法思想.
標(biāo)記-清除算法 (Mark-Sweep)
這是最基礎(chǔ)的收集算法, 分為標(biāo)記、清除兩個(gè)階段
主要算法思想就是
通過(guò)算法標(biāo)記出回收對(duì)象(舉例: hotspot使用可達(dá)性分析),進(jìn)而回收標(biāo)記的對(duì)象占用的空間

從示例圖不難看出, 該算法的最大缺陷就
- 內(nèi)存碎片化
后續(xù)碰到分配大對(duì)象時(shí)(連續(xù)的內(nèi)存空間), 必然導(dǎo)致內(nèi)存不夠從而觸發(fā)額外的GC.
另外就是標(biāo)記和清除兩個(gè)過(guò)程本身的效率都不高.
標(biāo)記-復(fù)制算法(copying)
復(fù)制算法算是Mark-Sweep算法的升級(jí)版, 主要就是為了解決Mark-Sweep算法“內(nèi)存碎片化”的問(wèn)題.
該算法的主要思想如下
按內(nèi)存容量將內(nèi)存劃分為等大小的兩塊. 每次只使用其中一塊,當(dāng)這一塊內(nèi)存滿后將尚存活的對(duì)象復(fù)制到另一塊上去, 把已使用的內(nèi)存清掉.

這種算法雖然實(shí)現(xiàn)簡(jiǎn)單,內(nèi)存效率高,不易產(chǎn)生碎片,但是存在兩個(gè)較嚴(yán)重問(wèn)題
可用內(nèi)存被壓縮到了原來(lái)的一半
存活對(duì)象較多的話, Copying算法的效率較低
標(biāo)記-整理算法(Mark-Compact)
為了解決以上兩種算法的缺陷, 進(jìn)而提出了標(biāo)記整理算法.
算法的主要思想如下
分為標(biāo)記、整理兩個(gè)階段
標(biāo)記階段和Mark-Sweep相同, 不同點(diǎn)是標(biāo)記后不會(huì)清理對(duì)象, 而是將存活對(duì)象移向內(nèi)存的一端.
然后清除端邊界外的對(duì)象.
image.png
分代收集算法
上面介紹了幾個(gè)算法都各有優(yōu)缺點(diǎn), 但沒(méi)有哪個(gè)是絕對(duì)優(yōu)勢(shì)的.
只能說(shuō)每個(gè)算法都有各自的應(yīng)用場(chǎng)景.
而在JVM垃圾回收領(lǐng)域, 面對(duì)各種內(nèi)存回收的復(fù)雜場(chǎng)景, 顯然, 不可能存在一種算法就能達(dá)到最優(yōu)解.
此時(shí)聰明的開(kāi)發(fā)者就提出了一種想法, 既然無(wú)法“一招通殺”, 那么, 我就“分而治之”.
通過(guò)一定規(guī)則把內(nèi)存區(qū)域劃成幾塊, 這樣某些小塊的內(nèi)存回收?qǐng)鼍熬痛嬖谀硞€(gè)“最優(yōu)解回收算法”, 每塊都是最優(yōu)解, 那么總體上不就是最優(yōu)解么?!
于是, 分代收集算法應(yīng)運(yùn)而生.
嚴(yán)格來(lái)說(shuō), 分代收集算法并不是個(gè)垃圾回收算法, 而是把對(duì)象按生命周期來(lái)進(jìn)行內(nèi)存劃分的思想.
該算法的主要思想如下
根據(jù)對(duì)象存活的不同生命周期, 將內(nèi)存劃分為幾塊不同的區(qū)域.
一般情況下將Java堆劃分為新生代和老年代
新生代的對(duì)象特點(diǎn)是大部分對(duì)象都是朝生夕死,生命周期很短, 每次垃圾回收時(shí)有大量對(duì)象需要被回收
老年代的對(duì)象特點(diǎn)是生命周期較長(zhǎng),每次垃圾回收時(shí)只有少量對(duì)象需要被回收
結(jié)合新生代、老年代的特點(diǎn), 于是適配了合適的垃圾回收算法
新生代與復(fù)制算法
目前大部分JVM 的 GC 對(duì)于新生代都采取 Copying 算法,
因?yàn)樾律看卫厥斩家厥沾蟛糠炙劳鰧?duì)象,存活的對(duì)象少, 所以要復(fù)制的操作比較少.
這樣的特點(diǎn)剛好能發(fā)揮Copying 算法的效率.
新生代的劃分并沒(méi)有嚴(yán)格按Copying 算法的1:1劃分法, 而是將新生代劃分為一塊較大的 Eden區(qū)和兩個(gè)較小的 Survivor區(qū)(From區(qū), To區(qū))(一般也稱為S1和S2區(qū)),
默認(rèn)內(nèi)存占比為 Eden:S1:S2 是8:1:1

每次使用Eden區(qū)和其中的一塊 Survivor 區(qū),當(dāng)進(jìn)行垃圾回收時(shí),將該兩塊區(qū)中還存活的對(duì)象復(fù)制到另一塊 Survivor區(qū)中.
老年代與標(biāo)記整理算法
老年代本身存放的對(duì)象都是熬過(guò)了一輪輪GC的, 都是“存活幾率”較高的, 老年代最終存放著大量的對(duì)象, 所以每次只需對(duì)少量死亡對(duì)象進(jìn)行回收, 因而采用 Mark-Compact 算法.
一次完整的GC過(guò)程如下
實(shí)例理解:
-
新New的對(duì)象一般出現(xiàn)在Eden區(qū)
- PS: 少數(shù)大對(duì)象(需要連續(xù)的內(nèi)存空間) 會(huì)直接進(jìn)老年代
- PS: Hotspot可配置: -XX:PretenureSizeThreshold=2m , 即2m以上的對(duì)象直接進(jìn)老年代
-
慢慢的Eden區(qū)滿了, 此時(shí)觸發(fā)一次GC, 將還存活的對(duì)象復(fù)制到某個(gè)空的S區(qū), 稱為S1區(qū)
- PS: S1和S2身份隨時(shí)互換, 只有空的我們稱為S1區(qū), 兩個(gè)S區(qū)必然有一個(gè)是空的
- PS: 也就是假設(shè)年輕代空間比例8:1:1
慢慢的S1區(qū)也滿了, 此時(shí)觸發(fā)GC, 已滿的S1區(qū)和Eden區(qū)還存活的對(duì)象
對(duì)象的內(nèi)存分配主要在新生代的 Eden區(qū)和 From區(qū), 少數(shù)情況(比如new了個(gè)大對(duì)象, 新生代放不下了)會(huì)直接分配到老生代
- 當(dāng)新生代的 Eden Space 和 From Space 空間不足時(shí)就會(huì)發(fā)生一次 GC,進(jìn)行 GC 后,Eden Space 和 From Space 區(qū)的存活對(duì)象會(huì)被挪到 To Space,然后將 Eden Space 和 From Space 進(jìn)行清理。
- 如果 To Space 無(wú)法足夠存儲(chǔ)某個(gè)對(duì)象,則將這個(gè)對(duì)象存儲(chǔ)到老生代。
- 在進(jìn)行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反復(fù)循環(huán)。
- 當(dāng)對(duì)象在 Survivor 區(qū)躲過(guò)一次 GC 后,其年齡就會(huì)+1。默認(rèn)情況下年齡到達(dá) 15 的對(duì)象會(huì)被 移到老生代中。
請(qǐng)關(guān)注我的訂閱號(hào)

參考
- 《深入理解JAVA虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐》
