thinkphp做網(wǎng)站好嗎免費網(wǎng)站注冊免費創(chuàng)建網(wǎng)站
目錄
- 一、什么是垃圾回收?
- 1.1 什么是垃圾回收?
- 1.2 什么對象能被垃圾回收?
- 1)引用計數(shù)法
- 2)可達性分析算法
- 二、JVM 垃圾回收算法
- 2.1 標記清除算法
- 2.2 標記整理算法(標記壓縮算法)
- 2.3 復制算法
- 2.4 總結(jié)
- 三、JVM 的分代回收
- 3.1 堆中的區(qū)域劃分
- 3.2 分代收集算法-工作機制
- 3.3 Minor GC、Mixed GC、Full GC 的區(qū)別是什么?
- 3.4 總結(jié)
- 四、JVM 有哪些垃圾回收器?
- 4.1 單線程垃圾收集器
- 4.2 多線程垃圾收集器
- 4.3 并發(fā)垃圾收集器
- 4.4 G1垃圾收集器
- 五、G1垃圾收集器
- 5.1 G1垃圾回收器的特點
- 5.2 垃圾回收的三個階段
- 1)Young Collection(新生代垃圾回收)
- 2)Young Collection + Concurrent Mark(年輕代垃圾回收+并發(fā)標記)
- 3)Mixed Collection(混合垃圾回收)
- 5.2 Humongous區(qū)
- 六、總結(jié)
一、什么是垃圾回收?
1.1 什么是垃圾回收?
在介紹垃圾回收之前,我們需要先明確幾個點:
- 為什么要進行垃圾回收呢?
- 回收哪里的垃圾呢?
是這樣的,垃圾回收主要指的是回收堆中的對象,堆的位置如下圖所示:
堆是一個共享區(qū)域,我們創(chuàng)建的對象和數(shù)組都存儲在當前的堆里。但是我們也不能無限地去創(chuàng)建對象,也不是所有的對象都需要一直存在,如果說不進行垃圾回收的話,內(nèi)存遲早都會被耗盡的,所以說及時的垃圾回收就顯得非常有必要了。那什么對象才能被回收呢?
1.2 什么對象能被垃圾回收?
簡單一句話就是:如果一個或多個對象沒有任何的引用指向它了,那么這個對象現(xiàn)在就可能會被垃圾回收器回收。
定位什么樣的對象是垃圾有兩種方式:第一個是引用計數(shù)法,第二個是可達性算法。
1)引用計數(shù)法
引用計數(shù)法
:一個對象被引用了一次,在當前的對象頭上遞增一次引用次數(shù),如果這個對象的引用次數(shù)為0,代表這個對象可回收。
比如下面這段代碼:
String demo = new String("123");
因為我們在這里 new 了一個 String,所以會在堆中開辟一塊空間去存儲當前的對象:

這時,我們的引用計數(shù)法就會去增加一次引用的次數(shù),ref=1。
假如目前的 demo 指向 null:
String demo = null;
這時,demo 就不會指向原來的那塊內(nèi)存了,當前的 ref 就會從 1 變成 0:

ref 變成 0 就表示當前的對象 new String(“123”) 是可以被垃圾回收的。
這個方法看起來非常簡單,但是引用計數(shù)法也有一定的問題,我們來看下一個例子:
假如有下面這樣一段代碼:
我們先來分析一下這個代碼:
- 左邊代碼中,Demo 類中有一個 Demo 的屬性指向它自己,然后還有一個 String 的 name 和一個帶參構(gòu)造函數(shù)。
- 右邊代碼中,使用了 Demo 類創(chuàng)建了 a 和 b 兩個實例,其中 a 的 instance 成員變量指向了 b,b 的 instance 成員變量指向了 a。
這段代碼在內(nèi)存中的表示方式如下:

首先,在棧中有一個變量 a 和一個變量 b,分別指向堆中的兩塊內(nèi)存。由于除了 a 和 b 指向了這兩塊內(nèi)存,它們之間還存在互相引用,只要有對象引用,引用次數(shù)就會加1,所以此時引用次數(shù) ref=2。
下面我們繼續(xù)執(zhí)行代碼:

代碼中把 a 和 b 都指向 null,這時我們棧中的變量 a 和變量 b 就不再引用堆中那兩塊內(nèi)存了,這樣堆中兩塊內(nèi)存的引用次數(shù) ref 就會都變成 1 了,如下所示:

這時大家就會發(fā)現(xiàn),目前堆中的這兩個對象的引用次數(shù)都是1,但是目前這兩個對象是沒有人使用的,但是它們依然不會被回收。如果出現(xiàn)了這種情況,就是出現(xiàn)了 循環(huán)引用,就會引發(fā)內(nèi)存泄漏。因為這兩個對象一直不會被回收。

以上就是引用計數(shù)法,使用起來是非常簡單,但是缺點就是容易導致內(nèi)存泄漏。所以說一般不會采用這種方法去定位某個對象是否是垃圾。
下面我們看第二種定位垃圾的方式,可達性分析算法。
2)可達性分析算法
現(xiàn)在的虛擬機采用的都是通過可達性算法來確定哪些對象是垃圾。
首先,我們來看下面這張圖:

最上面有一個 GC Roots
,相當于是一個樹根。從根中探索查看是否有關(guān)聯(lián)的對象:
- 如果說能關(guān)聯(lián)到,不管是直接關(guān)聯(lián)(如A)還是間接關(guān)聯(lián)(如B),找到的這些對象都是存活的對象,這些對象就不會被垃圾回收。
- 如果掃描堆的過程中,不能沿著 GC Roots 找到的對象,比如X、Y,它們目前沒有與任何的 GC Root 進行關(guān)聯(lián),這兩個就證明是可以被回收的對象。
對比我們剛才說的引用計數(shù)法,可達性分析算法能夠更精確地定位哪些是可回收的垃圾。所以現(xiàn)在的虛擬機都是采用可達性分析算法來去確定哪些是垃圾。
其實這里還有一個問題,就是哪些對象可以作為 GC Root 呢?
一共有4種對象可以作為 GC Root:
-
虛擬機棧(棧幀中的本地變量表)中引用的對象: 比如下面這個示例中的 demo:
-
方法區(qū)中類靜態(tài)屬性引用的對象: 比如下面這個示例中,變量a 作為靜態(tài)變量,它所引用的 new Demo() 也可以作為 GC Root。
-
方法區(qū)中常量引用的對象: 比如下面這個示例中,變量a 作為靜態(tài)常量,這種情況下,變量a 所引用的 new Demo() 也是可以作為 GC Root 的。
-
本地方法棧中 JNI(即一般說的 Native 方法)引用的對象: 通過 JNI 接口由本地代碼(如C/C++)持有的對象引用。
-
系統(tǒng)類加載器加載的類: 由系統(tǒng)類加載器加載的類,這些類本身以及它們通過靜態(tài)字段持有的對象通常被認為是不可回收的。
在我們平時的開發(fā)過程中,用到前三種的對象類型會多一些。以上就是可達性分析算法。
二、JVM 垃圾回收算法
上面我們已經(jīng)能夠區(qū)分出內(nèi)存中存活的對象和死亡的對象,GC接下來的任務就是去執(zhí)行垃圾回收,釋放掉那些無用對象所占用的內(nèi)存空間,以便有足夠的空間為新對象分配可用的內(nèi)存。
目前 JVM 中就有三種比較常見的垃圾回收算法:
- 標記清除算法
- 復制算法
- 標記整理算法
這三種算法每一個都有各自的特點,下面一起了解一下。
2.1 標記清除算法
標記清除
是將垃圾回收分為 2 個階段,分別是標記和清除:
- 對這些標記為可回收的內(nèi)容進行 垃圾回收。
我們先來看下面這張圖:

圖中藍色部分代表 “存活的對象”,灰色部分代表 “待回收的對象”,其他的都是 “空閑的空間”。
標記清除算法的第1步就是:根據(jù)可達性分析算法得出的垃圾進行 標記。
可達性分析算法就是使用 GC Root 去標記哪些是存活的對象,如下所示,可以看到藍色部分都是被 GC Root 標記存活的對象:

標記清除算法的第2步就是:對這些標記為可回收的內(nèi)容進行垃圾回收。
根據(jù)步驟,標記的對象都留下來,然后直接清除沒有標記的內(nèi)容就可以了。回收后的效果如下:

現(xiàn)在這個圖中只留下了藍色的部分,這些是做了標記,屬于目標存活的對象,這個就是 標記清除算法
。
- 優(yōu)點: 標記和清除速度較快。
- 缺點: 內(nèi)存碎片化較為嚴重,內(nèi)存不連貫。
我們知道數(shù)組也是會存儲到堆中,并且數(shù)組存儲必須是一個連續(xù)的內(nèi)存空間。如果我們使用了標記清除算法,由于內(nèi)存不連續(xù),有可能沒有辦法進行存儲新的對象,也沒有辦法去存儲一個比較大的數(shù)組。所以說這個算法用的相對比較少一些,用的比較多的主要是后面兩種。
我們繼續(xù)來看第二種:
2.2 標記整理算法(標記壓縮算法)
“標記整理算法”,也稱標記壓縮算法,與我們剛才介紹的 “標記清除算法” 差不多,我們先看下面這張圖:

首先,它也會通過 GC Root 去標記哪些對象是存活的對象,然后再去清除待回收的對象。但是它同時還多了一步,我們再來看下面這張圖:

從圖中可以看到,標記整理算法清除之后,它會把存活的對象進行整理,就是把所有的對象像一端移動。這樣就避免了內(nèi)存碎片化的問題了。但是由于標記整理算法多了一步,需要移動對象在內(nèi)存中的位置,所以說它的性能也會收到一定的影響。
以上就是標記整理算法,很多老年代的垃圾回收器都是使用標記整理算法。
下面我們再來看最后一種垃圾回收算法:復制算法。
2.3 復制算法
復制算法
是將整個內(nèi)存分成了大小相等的區(qū)域,標記階段與我們前面的算法是類似的,也是通過 GC Root 進行標記哪些對象是存活的對象,然后將存活的對象進行復制,復制到另外一塊內(nèi)存區(qū)域。

當然,復制的過程中就自動完成了碎片的整理。如下圖所示,就是回收之后的效果:

左邊的 4 個對象被挪到了右邊的區(qū)域,接下來把左邊整個區(qū)域清空就可以了。
優(yōu)點:
- 在垃圾對象多的情況下,效率較高;
- 清理后,內(nèi)存無碎片。
缺點:
- 分配的 2 塊內(nèi)存空間,在同一個時刻,只能使用一半,內(nèi)存使用率較低。
一般垃圾回收的時候,年輕代的垃圾回收器都會使用復制算法。
以上就是全部的三種垃圾回收算法了,下面我們進行一下總結(jié)。
2.4 總結(jié)
JVM 垃圾回收算法有哪些?
- 標記清除算法:垃圾回收分為 2 個階段,分別是:標記、清除。效率高,有磁盤碎片,內(nèi)存不連續(xù)。
- 標記整理算法:與標記清除算法一樣,將存活的對象都向內(nèi)存另一端 移動,然后清理邊界以外的垃圾。無碎片,對象需要移動,效率低。
- 復制算法:將原有的內(nèi)存空間一分為二,每次只用其中的一塊。將正在使用的對象 復制 到另一個內(nèi)存空間中,然后將該內(nèi)存空間清空,交換兩個內(nèi)存的角色,完成垃圾的回收。無碎片,內(nèi)存使用率低。
三、JVM 的分代回收
在之前介紹 JVM 組成的時候說話,在堆中分為新生代和老年代。下面我們就要詳細說明一下,在垃圾回收的時候它們到底有什么作用。
3.1 堆中的區(qū)域劃分
首先,我們還是要重新介紹一下堆中的組成:
- 在 Java8 中,堆被分成了兩份:新生代和老年代(比例為1:2)
如下圖所示:
左邊的新生代占了 1/3,右邊的老年占了 2/3。在新生代中又劃分為了三份:
- 一個是伊甸園區(qū)Eden,新生的對象都分配到這里。
- 還有兩個幸存者區(qū)Survivor,分為 from 和 to。
- Eden區(qū)、from區(qū)、to區(qū)比例為 8:1:1。
以上就是目前堆中的結(jié)構(gòu)劃分,下面我們來介紹一下垃圾產(chǎn)生之后它們是如何工作的。
3.2 分代收集算法-工作機制
- 首先,我們剛才介紹過,新生的對象都需要存儲到 Eden 區(qū),如下圖所示:
- 當伊甸園區(qū)內(nèi)存不足的時候(如上圖所示已經(jīng)占滿了),這個時候 JVM 就會使用之前我們講過的 “可達性分析算法” 來去標記 Eden 區(qū)和 from 區(qū)中存活的對象,當然目前 from 區(qū)中是沒有對象的。
- 假如標記了 A 對象是存活的,接下來就會采用 “復制算法” 將 A 對象復制到 to 區(qū)中。復制完畢之后,Eden 區(qū)和 from 區(qū)都要清空掉。操作后如下所示:
- 假如經(jīng)過一段時間后,Eden 區(qū)內(nèi)存又出現(xiàn)不足,如下所示:
- 這時候依然會采用 “可達性分析算法” 去標記 Eden 區(qū)和 to 區(qū)中存活的對象,然后把這些對象復制到 from 區(qū)中。
- 假如現(xiàn)在 1 和 A 這兩個對象依然存活,這時候就直接復制到了 from 區(qū)中,然后 Eden 區(qū)和 to 區(qū)都會清空掉。操作后如下所示:
- 又經(jīng)過了一段時間,又有一些新的對象存到了 Eden 區(qū)中,如下所示:
- 假如這時候 A 對象被挪動的次數(shù)太多,比如超過了 15 次,那么這時候 A 對象就不會再在 from 區(qū)和 to 區(qū)之間挪過來挪過去了。它會直接把 A 對象存儲到 老年代 中。因為這種情況下,我們一般認為 A 對象會被一直引用著,它的存活時間會更長一些。
- 當然還有一種情況,假如我們的幸存者區(qū)已經(jīng)內(nèi)存不足了,或者說當前某個對象太大了,它也會提前晉升到老年代。例如下圖中,A 對象代表挪動次數(shù)超過 15 次之后進入了老年代;而 w 對象是一個新生的對象,它并沒有到達一定的挪動次數(shù),所以 w 對象是正常地在新生代中進行復制挪動,等 w 對象挪動到一定次數(shù)也會進入到老年代中。
以上就是新生代和老年代工作的配合方式,但是在實際面試過程中還會問一些名詞,比如:MinorGC、Mixed GC、FullGC。
3.3 Minor GC、Mixed GC、Full GC 的區(qū)別是什么?
我們前面了解到新生代和老年代的比例如下所示:
Minor GC
,也稱 Young GC,發(fā)生在新生代的垃圾回收,暫停時間短(STW)。Mixed GC
,新生代 + 老年代兩塊 部分 區(qū)域的垃圾回收,G1 收集器特有。Full GC
,新生代+老年代 完整 垃圾回收,暫停時間長(STW),應盡量避免。
名詞解釋:
STW
(Stop-The-World):,暫停所有應用程序線程,等待垃圾回收的完成。Minor GC 暫停時間短就說明效率比較高,如果暫停時間比較長就說明效率降低。
3.4 總結(jié)
1)堆的區(qū)域劃分
- 堆被分為了兩份:新生代和老年代【1:2】。
- 對于新生代,內(nèi)部又被分為了三個區(qū)域:Eden區(qū),幸存者區(qū)survivor(分成 from 和 to)【8:1:1】。
2)對象分代回收策略
- 新創(chuàng)建的對象,都會先分配到 Eden 區(qū)。
- 當 Eden 區(qū)內(nèi)存不足,標記 Eden 區(qū)與 from 區(qū)(現(xiàn)階段沒有)的存活對象。
- 將存活對象采用復制算法復制到 to 區(qū)中,復制完畢后,Eden 區(qū)和 from 內(nèi)存都得到釋放。
- 經(jīng)過一段時間后,Eden 區(qū)的內(nèi)存又出現(xiàn)不足,標記 Eden 區(qū)和 to 區(qū)存活的對象,將其復制到 from 區(qū)。
- 當幸存者區(qū)對象熬過幾次回收(最多15次),晉升到老年代(幸存者區(qū)內(nèi)存不足或大對象會提前晉升)。
四、JVM 有哪些垃圾回收器?
在 jvm 中,實現(xiàn)了多種垃圾收集器,包括:
- 單線程垃圾收集器(Serial 和 Serial Old)。
- 多線程垃圾收集器(Parallel Scavenge 和 Parallel Old),JDK8默認。
- 并發(fā)垃圾收集器(ParNew 和 CMS)。
- 智能并發(fā)垃圾收集器(G1),JDK9默認。
我們根據(jù)新生代和老年代可以進行如下分類:
- 新生代收集器: Serial、ParNew、Parallel Scavenge。
- 老年代收集器: Serial Old、CMS、Parallel Old。
- 通用收集器: G1。
常用組合:Serial + Serial Old,Parallel Scavenge + Parallel Old,ParNew + CMS,G1(不需要組合)
4.1 單線程垃圾收集器
單線程垃圾收集器
,也稱串行垃圾收集器,包含了兩個收集器:Serial 和 Serial Old,是指使用單線程進行垃圾回收,堆內(nèi)存較小,適合個人電腦。
- Serial: 作用于新生代,采用復制算法。
- Serial Old: 作用于老年代,采用標記-整理算法。
垃圾回收時,只有一個線程在工作,并且 Java 應用中的所有線程都要暫停(STW),等待垃圾回收的完成。
舉個例子,如下圖所示:
當前一個電腦里面有4核 CPU,綠色的線表示我們的程序在正常的運行,當?shù)竭_安全點后就會產(chǎn)生一個垃圾回收。這時候Serial 垃圾回收器會暫停其它線程,保留一個專門的線程來進行垃圾回收。等垃圾回收線程執(zhí)行完了垃圾回收之后,才能恢復運行程序的線程繼續(xù)運行。
4.2 多線程垃圾收集器
多線程垃圾收集器
,也稱并行垃圾收集器,也包含了兩個收集器:Parallel New 和 Parallel Old。JDK8默認使用此垃圾回收器。
- Parallel New: 作用于新生代,采用復制算法。
- Parallel Old: 作用于老年代,采用標記-整理算法。
垃圾回收時,多個線程在工作,并且 Java 應用中的所有線程都要暫停(STW),等待垃圾回收的完成。
舉個例子,如下圖所示:
當前一個電腦里面有4核 CPU,對應的多個線程都在同時進行垃圾回收,對比我們剛才說的串行垃圾回收器,有更多的線程參與了垃圾回收。這個性能的確會更高一些,但是在進行垃圾回收的時候,依然也會進行線程的中斷(STW)。JDK8 默認使用這種垃圾回收器
4.3 并發(fā)垃圾收集器
并發(fā)垃圾收集器
,也包含了兩個垃圾收集器:ParNew 和 CMS。CMS 全程 Concurrent Mark Sweep,是一款并發(fā)的、使用標記-清除算法的垃圾回收器。該回收器是針對老年代垃圾回收的,是一款以獲取最短回收停頓時間為目標的收集器,停頓時間短,用戶體驗就好。其最大特點是在進行垃圾回收時,應用仍然能正常運行。
舉個例子,如下圖所示:
當前一個電腦里面有4核 CPU:
-
在進行垃圾回收的時候,首先它會有一個初始標記,這個標記就是使用我們之前講過的 “可達性分析算法” 來標記存活的對象的,這時候會進行 STW,即標記的時候其他線程會進行阻塞狀態(tài)。
-
然后再進行并發(fā)標記,重新標記。為了方便理解,我們看下面這張圖:
-
情況一:當我們的代碼在執(zhí)行過程中,有可能在并發(fā)標記完成之后,本來 X 被認為是一個垃圾對象,但是在并發(fā)標記階段的同時,我們的代碼是可以正常運行的,也有可能會出現(xiàn)新的引用,比如:A對象又去引用了 X對象。那這個時候 X對象就不能被回收了,所以說我們在這里有一個重新標記的階段。
-
情況二:比如說,在一開始標記的時候,當前的 D對象的確是一個存活的對象,JVM 在重新標記的過程中,B對象取消了對 D對象的引用,那么此時的 D對象就要把它作為一個垃圾對象進行回收。
以上兩種情況就體現(xiàn)了重新標記的重要作用,我們再回到并發(fā)垃圾收集器的回收過程:
- 重新標記完成之后,才真正進入到并發(fā)清理的階段。從圖中可以看到,并發(fā)清理的過程中,其他線程還是可以正常運行的。
關(guān)于并發(fā)垃圾收集器,主要關(guān)注點在于停頓時間短,記住這點即可。
4.4 G1垃圾收集器
由于 G1 垃圾收集器比較重要,也是面試官常問的內(nèi)容,我們單獨作為一個章節(jié)進行講解。
五、G1垃圾收集器
G1垃圾收集器
是一種智能并發(fā)垃圾收集,應用于新生代和老年代,在 JDK9 之后默認使用 G1。
5.1 G1垃圾回收器的特點
- 在 G1 垃圾收集器中劃分了多個區(qū)域,每個區(qū)域都可以充當 Eden區(qū)、Survivor區(qū)、Old(老年代)、Humongous,其中 humongous 專為大對象準備。這里面并沒有之前的老年代和新生代的比例劃分,也沒有新生代中的比例劃分,每個區(qū)域都一樣,并且每個區(qū)域可以存儲各種對象,還專門增加了一個 humongous 區(qū)用來存儲大對象。
- 采用復制算法,沒有內(nèi)存碎片。
- 響應時間與吞吐量兼顧,即效率高的同時處理任務比較多。
- G1垃圾收集器在垃圾回收的過程中主要分為三個階段:新生代回收、并發(fā)標記、混合收集。如下圖所示:

- 如果并發(fā)失敗(或者說垃圾回收失敗,即回收速度趕不上創(chuàng)建新對象速度),會觸發(fā) FullGC。
以上就是 G1 垃圾回收器的特點了,下面我們就從 G1垃圾回收器的工作流程入手,進行了解。
5.2 垃圾回收的三個階段
1)Young Collection(新生代垃圾回收)
首先,我們來看一張圖:

這張圖表示了整個堆空間,我們把整個堆劃分成了大小相等的區(qū)域。其中的每個區(qū)域都可以作為我們剛才提到的 Eden區(qū)、Survivor區(qū)、Old(老年代)、Humongous。
- 初始時,所有區(qū)域都處于空閑狀態(tài)。
- 當創(chuàng)建對象時,就會挑出一些空閑區(qū)域作為 Eden區(qū)來存儲這些對象。比如下圖中就表明了幾個 E,這些就是我們的 Eden區(qū):

- 隨著對象越來越多,堆中的 Eden區(qū)可能快要放滿了,這時就會觸發(fā)一個新生代的垃圾回收。
大家可能有疑問:還有這么多的空間不去存儲就開始垃圾回收了嘛?是這樣的,G1垃圾收集器中新生代的占比不是固定的,它是在 5% ~ 6% 之間波動,G1會自動進行調(diào)整。但是大家可以思考一下,既然新生代的大小是被限制的,不管怎么波動都會有限制,都會限制當前的 Eden 區(qū)的大小,我們就不能隨便地去創(chuàng)建新對象。所以說當我們的 Eden 區(qū)的數(shù)量達到一定程度后,它就會觸發(fā)一次 Eden 區(qū)的垃圾回收。它也是采用復制算法,即會使用 “可達性分析算法” 去標明哪些對象是存活下來的。把所有存活的對象進行標記之后,沒有標記的對象就成為垃圾了。G1就會將這些存活的對象用復制算法復制到 Survivor區(qū)中。
如下圖所示:

- 圖中的 s 其實就是 Survivor 區(qū),經(jīng)過標記之后,將剩下的存活對象都復制到幸存者區(qū),然后之前的 Eden 區(qū)就可以釋放掉了,如下圖所示:

大家注意,在標記的過程當中或者是剛剛復制的過程中,都需要暫停,即觸發(fā)一個 STW。不過因為我們幸存者的對象相對來說是比較少的,所以暫停時間也不會太長。
以上就是新生代的第一次垃圾回收。隨著時間的流逝,還會產(chǎn)生很多的對象,如下圖所示:

后續(xù)產(chǎn)生的對象都會分配到 Eden區(qū),所以 Eden區(qū)的內(nèi)存就逐漸耗盡了。這時垃圾回收器就會把 Eden區(qū)中的幸存者對象和 Survivor區(qū)中的幸存者對象合并到一起,復制到一個新的幸存者區(qū)中。如下圖所示:

圖中出現(xiàn)了一個新的幸存者區(qū),然后新的幸存者區(qū)和之前的幸存者區(qū)就會進行一次復制。
當然,這里還有一種情況,如果當前的幸存者區(qū)中有一些對象已經(jīng)超過了閾值,這些比較老的對象就會晉升到老年代中。也就是說,如果一個對象經(jīng)過了很多次的垃圾回收,它就會晉升到老年代,另外一部分幸存者區(qū)和伊甸園區(qū)中的幸存者對象就會復制到一個新的幸存者區(qū)中。這樣一來,伊甸園區(qū)和上一次的幸存者區(qū)就可以釋放了。
以上就是垃圾回收的第一個階段:年輕代的垃圾回收。下面我們進入第二個階段:并發(fā)的標記階段。
2)Young Collection + Concurrent Mark(年輕代垃圾回收+并發(fā)標記)
在當前的階段中有一個觸發(fā)條件:
- 當老年代占用內(nèi)存超過閾值(默認是45%)后,觸發(fā)并發(fā)標記,這時無需暫停用戶線程。
其實,在老年代中找到那些存活的對象,并且給它們加上標記,這個過程是并發(fā)執(zhí)行的,在此期間不會暫停用戶的線程。當然這個階段也有一次重新標記,需要處理那些漏標的對象,所以說還是需要一次 STW 的,即需要暫停用戶線程。這個就是并發(fā)標記的階段了。

并發(fā)標記完成以后,就可以進入到第三個階段了,也就是混合收集階段。
3)Mixed Collection(混合垃圾回收)
在混合收集階段,它并不是一次性收集了所有的老年代的內(nèi)存,那樣暫停的時間會比較長,達不到預期的暫停時間。在 JVM 里面設置了一個預期的暫停時間,比如暫停時間不能超過XXms,這個是可以設置的。為了讓老年代的垃圾回收不超過預期的時間,它并不是一次性把所有的老年代都進行回收,而是先挑選出回收價值比較高的對象。比如下圖中標紅的幾個老年代:

它們里面存活的對象數(shù)量比較少,所以回收的價值比較高,即進行少量的回收就可以獲得同樣的內(nèi)存空間。相反,其它的一些老年代對象因為存活對象比較多,回收的價值就比較低了。因此它會挑選回收價值比較高的老年代,連同我們的 Eden區(qū)和 Survivor區(qū)一起來做一次垃圾回收。這個就是混合收集(既包含了新生代的垃圾回收,也包含了老年代的垃圾回收)。
如下圖所示:

在混合收集階段中,Eden區(qū)和原有 Survivor區(qū)中存活的對象會復制到一個新的 Survivor區(qū),而 Survivor區(qū)中到達一定挪動次數(shù)的存活對象和原有老年代中的存活對象會復制到一個新的老年代中。等混合收集完成之后,內(nèi)存就得到釋放了,如下圖所示:

復制完成,內(nèi)存得到釋放,這樣就完成了一次混合收集。
當然,混合收集可能要重復執(zhí)行多次。因為第一次我們在預期的時間內(nèi)可能會收到了剛才的三個老年代區(qū),后續(xù)有可能再多執(zhí)行幾次混合收集,把剩下的老年代再重新標記,再將標記的內(nèi)存逐漸地釋放出來。這個就是我們第三階段的混合收集了。
當進行了多次混合收集之后,又會進入下一輪的新生代回收、并發(fā)標記、混合收集。
以上就是 G1 垃圾回收器的三個階段。JVM 里面還有可能出現(xiàn)并發(fā)失敗的情況,也就是我們垃圾回收的速度小于我們創(chuàng)建新對象的速度,此時就會觸發(fā)一次 FullGC,它的暫停時間就非常久了。但是經(jīng)歷了多次的 G1 垃圾回收器的三個階段回收,它們都是并發(fā)執(zhí)行的,用戶的暫停時間是比較短的。一般是不會觸發(fā) FullGC 的。
5.2 Humongous區(qū)
其實還有這里一個問題:如果說一個對象太大了,一個區(qū)域放不下的話,會存入到一個 Humongous區(qū)中。如果說一塊區(qū)域不夠的話,會分配一塊連續(xù)的區(qū)域進行存儲大對象,如下圖所示:

講到這里,G1 垃圾回收器就講完了,下面我們進行下總結(jié)。
六、總結(jié)
詳細聊一下 G1 垃圾回收器:
- 應用于新生代和老年代,在 JDK9 之后默認使用 G1。
- 劃分為多個區(qū)域,每個區(qū)域都可以充當 Eden、Survivor、Old、Humongous 區(qū),其中 Humongous 區(qū)專為大對象準備。
- 采用復制算法。
- 響應時間與吞吐量兼顧。
- 分成三個階段:新生代回收(STW)、并發(fā)標記(重新標記 STW)、混合收集。
- 如果并發(fā)失敗(即回收速度趕不上創(chuàng)建新對象速度),會觸發(fā) FullGC。
整理完畢,完結(jié)撒花~🌻
參考地址:
1.新版Java面試專題視頻教程,java八股文面試全套真題+深度詳解(含大廠高頻面試真題),https://www.bilibili.com/video/BV1yT411H7YK