我肝了萬字的Java垃圾回收,看完你還敢說不會?
今天來說說 Java 垃圾回收,高頻面試問題。
提綱附上,話不多說,直接干貨

1、什么是垃圾回收?
垃圾回收(Garbage Collection,GC):就是釋放垃圾占用的空間,防止內(nèi)存泄露。對內(nèi)存堆中已經(jīng)死亡的或者長時(shí)間沒有使用的對象進(jìn)行清除和回收。
2、垃圾在哪兒?

上圖可以看到程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧都是伴隨著線程而生死,這些區(qū)域不需要進(jìn)行 GC。
而方法區(qū)/元空間在 1.8 之后就直接放到本地內(nèi)存了,假設(shè)總內(nèi)存 2G,JVM 被分配內(nèi)存 100M, 理論上元空間可以分配 2G-100M = 1.9G,空間還是足夠的,所以這塊區(qū)域也不用管。
所以就只剩下堆了,java 對象實(shí)例和數(shù)組都是在堆上分配的,所以垃圾回收器重點(diǎn)照顧堆。
3、怎么發(fā)現(xiàn)它?
在發(fā)生 GC 的時(shí)候,Jvm 是怎么判斷堆中的對象實(shí)例是不是垃圾呢?
這里有兩種方式:
1、引用計(jì)數(shù)法
就是給對象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器的值就加 1,每當(dāng)有一個(gè)引用失效時(shí),計(jì)數(shù)器的值就減 1。任何時(shí)刻只要對象的計(jì)數(shù)器值為 0,那么就可以被判定為垃圾對象。
這種方式,效率挺高,但是 Jvm 并沒有使用引用計(jì)數(shù)算法。那是因?yàn)樵谀撤N場合下存在問題
比如下面的代碼,會出現(xiàn)循環(huán)引用的問題:
public?class?Test?{
????Test?test;
????public?Test(String?name)?{}
????public?static??void?main(String[]?args)?{
????????Test?a?=?new?Test("A");
????????Test?b?=?new?Test("B");
????????a.test?=?b;
????????b.test?=?a;
????????a?=?null;
????????b?=?null;
????}
}
即使你把 a 和 b 的引用都置為 null 了,計(jì)數(shù)器也不是 0,而是 1,因?yàn)樗鼈冎赶虻膶ο笥只ハ嘀赶蛄藢Ψ?,所以無法回收這兩個(gè)對象。
2、可達(dá)性分析法
這才是 jvm 默認(rèn)使用的尋找垃圾算法。
它的原理是通過一些列稱為“GC Roots”的對象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜素所走過的路叫做稱為引用鏈“Reference Chain”,當(dāng)一個(gè)對象到 GC Roots 沒有任何引用鏈時(shí),就說這個(gè)對象是不可達(dá)的。

從上圖可以看到,即使 Object5 和 Object6 之間相互引用,但是沒有 GC Roots 和它們關(guān)聯(lián),所以可以解決循環(huán)引用的問題。
小知識點(diǎn):
1、哪些可以作為 GC ROOTS 根呢?
虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象 方法區(qū)中類靜態(tài)屬性引用的對象 方法區(qū)中常量引用的對象 本地方法棧中 JNI(即一般說的 Native 方法)引用的對象
2、不得不說的四種引用
強(qiáng)引用:就是在程序中普遍存在的,類似“Object a=new Object”這類的引用。只要強(qiáng)引用關(guān)系還存在,垃圾回收器就不會回收掉被引用的對象。 軟引用:用來描述一些還有用但是并非必須的對象。直到內(nèi)存空間不夠時(shí)(拋出 OutOfMemoryError 之前),才會被垃圾回收,通過 SoftReference 來實(shí)現(xiàn)。 弱引用:比軟引用還弱,也是用來描述非必須的對象的,當(dāng)垃圾回收器開始工作時(shí),無論內(nèi)存是否足夠用,弱引用的關(guān)聯(lián)的對象都會被回收 WeakReference。 虛引用:它是最弱的一種引用關(guān)系,它的唯一作用是用來作為一種通知。采用 PhantomRenference 實(shí)現(xiàn)。
3、為什么定義這些引用?
個(gè)人理解,其實(shí)就是給對象加一種中間態(tài),讓一個(gè)對象不只有引用和非引用兩種情況,還可以描述一些“食之無味棄之可惜”的對象。比如說:當(dāng)內(nèi)存空間足時(shí),則能保存在內(nèi)存中,如果內(nèi)存空間在進(jìn)行垃圾回收之后還不夠時(shí),才對這些對象進(jìn)行回收。
4、生存還是死亡?
要真正宣告一個(gè)對象死亡,至少要經(jīng)歷兩次標(biāo)記過程和一次篩選。
一張圖帶你看明白:

5、垃圾收集算法
1、標(biāo)記清除算法
分為兩個(gè)階段“標(biāo)記”和“清除”,標(biāo)記出所有要回收的對象,然后統(tǒng)一進(jìn)行清除。

缺點(diǎn):
在對象變多的情況下,標(biāo)記和清除效率都不高 會產(chǎn)生空間碎片
2、復(fù)制算法
就是將堆分成兩塊完全相同的區(qū)域,對象只在其中一塊區(qū)域內(nèi)分配,然后標(biāo)記出那些是存活的對象,按順序整體移到另外一個(gè)空間,然后回收掉之前那個(gè)區(qū)域的所有對象。

缺點(diǎn):
雖然能夠解決空間碎片的問題,但是空間少了一半。也太多了吧??!
3、標(biāo)記整理算法

這種算法是,先找到存活的對象,然后將它們向空間的一端移動,最后回收掉邊界以外的垃圾對象。
4、分代收集
其實(shí)就是整合了上面三種算法,揚(yáng)長避短。
之所以叫分代,是因?yàn)楦鶕?jù)對象存活周期的不同將整個(gè) Java 堆切割成為三個(gè)部分:
Young(年輕代) Eden(伊利園):新生對象 Survivor(幸存者):垃圾回收后還活著的對象 Tenured(老年代):對象多次回收都沒有被清理,會移到老年代 Perm(永久代):存放加載的類別還有方法對象,java8 之后移除了永久代,替換為元空間(Metaspace)
在新生代中,每次垃圾收集都有大量的對象死去,只有少量的存活,那就選用 復(fù)制算法 ,因?yàn)閺?fù)制成本很小,只需要復(fù)制少量存活對象。
老年代中,存活對象較多,沒有額外的空間擔(dān)保,就得使用 標(biāo)記清除 或者 標(biāo)記整理 。
6、垃圾收集器
在說垃圾回收器之前需要了解幾個(gè)概念:
1、幾個(gè)概念
吞吐量
CPU 用于運(yùn)行用戶代碼的時(shí)間與 CPU 總消耗時(shí)間的比值。
比如說虛擬機(jī)總運(yùn)行了 100 分鐘,用戶代碼時(shí)間 99 分鐘,垃圾回收時(shí)間 1 分鐘,那么吞吐量就是 99%。
STW
全稱 Stop-The-World,即在 GC 期間,只有垃圾回收器線程在工作,其他工作線程則被掛起。
為什么需要 STW 呢?
在 java 程序中引用關(guān)系是不斷會變化的,那么就會有很多種情況來導(dǎo)致垃圾標(biāo)識出錯。
想想一下如果一個(gè)對象 A 當(dāng)前是個(gè)垃圾,GC 把它標(biāo)記為垃圾,但是在清除前又有其他引用指向了 A,那么此刻又不是垃圾了。
那么,如果沒有 STW 的話,就要去無限維護(hù)這種關(guān)系來去采集正確的信息,顯然是不可取的。
安全點(diǎn)
從線程角度看,安全點(diǎn)可以理解成是在代碼執(zhí)行過程中的一些特殊位置,當(dāng)線程執(zhí)行到這些位置的時(shí)候,說明虛擬機(jī)當(dāng)前的狀態(tài)是安全的。
比如:方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等這些地方才會產(chǎn)生安全點(diǎn)。
如果有需要,可以在這個(gè)位置暫停,比如發(fā)生 GC 時(shí),需要暫停所有活動線程,但是線程在這個(gè)時(shí)刻,還沒有執(zhí)行到一個(gè)安全點(diǎn),所以該線程應(yīng)該繼續(xù)執(zhí)行,到達(dá)下一個(gè)安全點(diǎn)的時(shí)候暫停,等待 GC 結(jié)束。
串行、并行
串行:是指垃圾回收線程在進(jìn)行垃圾回收工作,此時(shí)用戶線程處于等待狀態(tài)。
并行:是指用戶線程和多條垃圾回收線程分別在不同 CPU 上同時(shí)工作。
2、回收器
下面是一張很經(jīng)典的圖,展示了 7 種不同分代的收集器,如果兩個(gè)收集器之間存在連線,說明可以搭配使用。

Serial
Serial 收集器是一個(gè)單線程收集器,在進(jìn)行垃圾回收器的時(shí)候,必須暫停其他工作線程,也就是發(fā)生 STW。在 GC 期間,應(yīng)用是不可用的。

特點(diǎn):1、采用復(fù)制算法 ?2、單線程收集器 ?3、效率會比較慢,但是因?yàn)槭菃尉€程,所以消耗內(nèi)存小
ParNew
ParNew 是 Serial 的多線程版本,也是工作在新生代,能與 CMS 配合使用。
在多 CPU 的情況下,由于 ParNew 的多線程回收特性,毫無疑問垃圾收集會更快,也能有效地減少 STW 的時(shí)間,提升應(yīng)用的響應(yīng)速度。

特點(diǎn):1、采用復(fù)制算法 ?2、多線程收集器 ?3、效率高,能大大減少 STW 時(shí)間。
Parallel Scavenge
Parallel Scavenge 收集器也是一個(gè)使用復(fù)制算法,多線程,工作于新生代的垃圾收集器,看起來功能和 ParNew 收集器基本一樣。

但是它有啥特別之處呢?關(guān)注點(diǎn)不同
ParNew 垃圾收集器關(guān)注的是盡可能縮短垃圾收集時(shí)用戶線程的停頓時(shí)間,更適合用到與用戶交互的程序,因?yàn)橥nD時(shí)間越短,用戶體驗(yàn)肯定就好呀??! Parallel Scavenge 目標(biāo)是達(dá)到一個(gè)可控制的吞吐量,所以更適合做后臺運(yùn)算等不需要太多用戶交互的任務(wù)。
Parallel Scavenge 收集器提供了兩個(gè)參數(shù)來控制吞吐量,
-XX:MaxGCPauseMillis:控制最大垃圾收集時(shí)間 -XX:GCTimeRati:直接設(shè)置吞吐量大小
特點(diǎn):1、采用復(fù)制算法 2、多線程收集器 3、吞吐量優(yōu)先
Serial Old
Serial 收集器是工作于新生代的單線程收集器,與之相對地,Serial Old 是工作于老年代的單線程收集器。
作用:
在 Client 模式下與 Serial 回收器配合使用 Server 模式下,則它還有兩大用途:一種是在 JDK 1.5 及之前的版本中與 Parallel Scavenge 配合使用,另一種是作為 CMS 收集器的后備預(yù)案,在并發(fā)收集發(fā)生 Concurrent Mode Failure 時(shí)使用
它與 Serial 收集器配合使用示意圖如下:

特點(diǎn):1、標(biāo)記-整理算法 2、單線程 3、老年代工作
Parallel Old
Parallel Old 是一個(gè)多線程的垃圾回收器,采用標(biāo)記整理算法,負(fù)責(zé)老年代的垃圾回收工作,可以與 Parallel Scavenge 垃圾回收器一起搭配工作。真正的實(shí)現(xiàn)吞吐量優(yōu)先
示意圖如下:

特點(diǎn):1、標(biāo)記-整理算法 2、多線程 3、老年代工作
CMS
CMS 可以說是一款具有"跨時(shí)代"意義的垃圾回收器,如果應(yīng)用很重視服務(wù)的響應(yīng)速度,希望給用戶最好的體驗(yàn),則 CMS 收集器是非常合適的,它是以獲取最短回收停頓時(shí)間為目標(biāo)的收集器!
CMS 雖然工作在老年代,和之前收集器不同的是,使用的標(biāo)記清除算法
示意圖如下:

垃圾回收的 4 個(gè)步驟:
初始標(biāo)記:標(biāo)記出來和 GC Roots 直接關(guān)聯(lián)的對象,整個(gè)速度是非??斓模瑫l(fā)生 STW,確保標(biāo)記的準(zhǔn)確性。 并發(fā)標(biāo)記:并發(fā)標(biāo)記這個(gè)階段會直接根據(jù)第一步關(guān)聯(lián)的對象找到所有的引用關(guān)系,耗時(shí)較長,但是這個(gè)階段會與用戶線程并發(fā)運(yùn)行,不會有很大的影響。 重新標(biāo)記:這個(gè)階段是為了解決第二步并發(fā)標(biāo)記所導(dǎo)致的標(biāo)錯情況。并發(fā)階段會和用戶線程并行,有可能會出現(xiàn)判斷錯誤的情況,這個(gè)階段就是對上一個(gè)階段的修正。 并發(fā)清除:最后一個(gè)階段,將之前確認(rèn)為垃圾的對象進(jìn)行回收,會和用戶線程一起并發(fā)執(zhí)行。
缺點(diǎn):
影響用戶線程的執(zhí)行效率:CMS 默認(rèn)啟動的回收線程數(shù)是(處理器核心數(shù) + 3)/ 4 ,由于是和用戶線程一起并發(fā)清理,那么勢必會影響到用戶線程的執(zhí)行速度 會產(chǎn)生浮動垃圾:CMS 的第 4 個(gè)階段并發(fā)清除是和用戶線程一起的,會產(chǎn)生新的垃圾,就叫浮動垃圾 會產(chǎn)生碎片化的空間:標(biāo)記清除的缺點(diǎn)
G1
全稱:Garbage-First
G1 回收的目標(biāo)不再是整個(gè)新生代或者是老年代。G1 可以回收堆內(nèi)存的任何空間來進(jìn)行,不再是根據(jù)年代來區(qū)分,而是那塊空間垃圾多就去回收,通過 Mixed GC 的方式去進(jìn)行回收。
先看下堆空間的劃分:

G1 垃圾回收器把堆劃分成大小相同的 Region,每個(gè) Region 都會扮演一個(gè)角色,分別為 H、S、E、O。
E 代表伊甸區(qū) S 代表 Survivor 區(qū) H 代表的是 Humongous 區(qū) O 代表 Old 區(qū)
G1 的工作流程圖:

初始標(biāo)記:標(biāo)記出來 GC Roots 能直接關(guān)聯(lián)到的對象,修改 TAMS 的值以便于并發(fā)回收時(shí)新對象分配 并發(fā)標(biāo)記:根據(jù)剛剛關(guān)聯(lián)的對像掃描整個(gè)對象引用圖,和用戶線程并發(fā)執(zhí)行,記錄 SATB(原始快照) 在并發(fā)時(shí)有引用的值 最終標(biāo)記:處理第二步遺留下來的少量 SATB(原始快照) 記錄,會發(fā)生 STW 篩選回收:維護(hù)之前提到的優(yōu)先級列表,根據(jù)優(yōu)先級列表、用戶設(shè)置的最大暫停時(shí)間來回收 Region
特點(diǎn):
并行與并發(fā):G1 能充分利用多 CPU、多核環(huán)境下的硬件優(yōu)勢,可以通過并發(fā)的方式讓 Java 程序繼續(xù)執(zhí)行,進(jìn)一步縮短 STW 的時(shí)間。 分代收集:分代概念在 G1 中依然得以保留,它能夠采用不同的方式去處理新創(chuàng)建的對象和已經(jīng)存活了一段時(shí)間、熬過多次 GC 的舊對象來獲得更好的收集效果。 空間整合:G1 從整體上看是基于標(biāo)記-整理算法實(shí)現(xiàn)的,從局部(兩個(gè) Region 之間)上看是基于復(fù)制算法實(shí)現(xiàn)的,G1 運(yùn)行期間不會產(chǎn)生內(nèi)存空間碎片。 可預(yù)測停頓:G1 比 CMS 厲害在能建立可預(yù)測的停頓時(shí)間模型,能讓使用者明確指定在一個(gè)長度為 M 毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不得超過 N 毫秒。
7、內(nèi)存分配與回收策略
上文說的一直都是回收內(nèi)存的內(nèi)容,那么怎么給對象分配內(nèi)存呢?
堆空間的結(jié)構(gòu):

Eden 區(qū)
研究表明,有將近 98%的對象是朝生夕死,所以針對這一現(xiàn)狀,大多數(shù)情況下,對象會在新生代 Eden 區(qū)中進(jìn)行分配。
當(dāng) Eden 區(qū)沒有足夠空間進(jìn)行分配時(shí),虛擬機(jī)會發(fā)起一次 Minor GC,Minor GC 相比 Major GC 更頻繁,回收速度也更快。
通過 Minor GC 之后,Eden 會被清空,Eden 區(qū)中絕大部分對象會被回收,而那些無需回收的存活對象,將會進(jìn)到 Survivor 的 From 區(qū)(若 From 區(qū)不夠,則直接進(jìn)入 Old 區(qū))。
Survivor 區(qū)
Survivor 區(qū)相當(dāng)于是 Eden 區(qū)和 Old 區(qū)的一個(gè)緩沖,Survivor 又分為 2 個(gè)區(qū),一個(gè)是 From 區(qū),一個(gè)是 To 區(qū)。每次執(zhí)行 Minor GC,會將 Eden 區(qū)和 From 存活的對象放到 Survivor 的 To 區(qū)(如果 To 區(qū)不夠,則直接進(jìn)入 Old 區(qū))。
問題 1:為什么需要 Survivor?
如果沒有 Survivor 區(qū),Eden 區(qū)每進(jìn)行一次 Minor GC,存活的對象就會被送到老年代,老年代很快就會被填滿。而有很多對象雖然一次 Minor GC 沒有消滅,但其實(shí)或許第二次,第三次就需要被清除。
這時(shí)候移入老年區(qū),很明顯不是一個(gè)明智的決定。
所以,Survivor 的存在意義就是減少被送到老年代的對象,進(jìn)而減少老年代 GC 的發(fā)生。Survivor 的預(yù)篩選保證,只有經(jīng)歷 15 次 Minor GC 還能在新生代中存活的對象,才會被送到老年代。
問題 2:為什么需要 From 和 To 兩個(gè)呢?
這種機(jī)制最大的好處就是可以解決內(nèi)存碎片化,整個(gè)過程中,永遠(yuǎn)有一個(gè) Survivor 區(qū)是空的,另一個(gè)非空的 Survivor 區(qū)是無碎片的。
假設(shè)只有一個(gè) Survivor 區(qū)。
Minor GC 執(zhí)行后,Eden 區(qū)被清空了,存活的對象放到了 Survivor 區(qū),而之前 Survivor 區(qū)中的對象,可能也有一些是需要被清除的。
那么問題來了,這時(shí)候我們怎么清除它們?
在這種場景下,我們只能標(biāo)記清除,而我們知道標(biāo)記清除最大的問題就是內(nèi)存碎片,在新生代這種經(jīng)常會消亡的區(qū)域,采用標(biāo)記清除必然會讓內(nèi)存產(chǎn)生嚴(yán)重的碎片化。
因?yàn)?Survivor 有 2 個(gè)區(qū)域,所以每次 Minor GC,會將之前 Eden 區(qū)和 From 區(qū)中的存活對象復(fù)制到 To 區(qū)域。第二次 Minor GC 時(shí),To 區(qū) 到 From 區(qū) ,以此反復(fù)。
Old 區(qū)
老年代占據(jù)著 2/3 的堆內(nèi)存空間,只有在 Major GC 的時(shí)候才會進(jìn)行清理,每次 GC 都會觸發(fā)“Stop-The-World”。內(nèi)存越大,STW 的時(shí)間也越長,所以內(nèi)存也不僅僅是越大就越好。
由于復(fù)制算法在對象存活率較高的老年代會進(jìn)行很多次的復(fù)制操作,效率很低,所以在這里老年代采用的是標(biāo)記整理算法。
下面三種情況也會直接進(jìn)入老年代:
大對象
大對象指需要大量連續(xù)內(nèi)存空間的對象,這部分對象不管是不是“朝生夕死”,都會直接進(jìn)到老年代。這樣做主要是為了避免在 Eden 區(qū)及 2 個(gè) Survivor 區(qū)之間發(fā)生大量的內(nèi)存復(fù)制。當(dāng)你的系統(tǒng)有非常多“朝生夕死”的大對象時(shí),需要注意。
長期存活對象
虛擬機(jī)給每個(gè)對象定義了一個(gè)對象年齡 Age 計(jì)數(shù)器。正常情況下對象會不斷的在 Survivor 的 From 區(qū)與 To 區(qū)之間移動,對象在 Survivor 區(qū)中每經(jīng)歷一次 Minor GC,年齡就增加 1 歲。當(dāng)年齡增加到 15 歲時(shí),這時(shí)候就會被轉(zhuǎn)移到老年代。
動態(tài)對象年齡
虛擬機(jī)并不重視要求對象年齡必須到 15 歲,才會放入老年區(qū),如果 Survivor 空間中相同年齡所有對象大小的總合大于 Survivor 空間的一半,年齡大于等于該年齡的對象就可以直接進(jìn)去老年區(qū)。
空間分配擔(dān)保
在發(fā)生 Minor GC 之前,虛擬機(jī)會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間。
如果條件成立的話,Minor GC 是可以確保安全的。
如果不成立,則虛擬機(jī)會查看 HandlePromotionFailure 設(shè)置是否擔(dān)保失敗,如果允許,那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小。
如果大于,嘗試進(jìn)行一次 Minor GC。
如果小于或者 HandlePromotionFailure 不允許,則進(jìn)行一次 Full GC。
End
看累了吧,學(xué)到了吧,那就關(guān)注一下唄!
學(xué)到就是賺到,歡迎在看、點(diǎn)贊、轉(zhuǎn)發(fā),您的認(rèn)可是我原創(chuàng)的動力!
