面試GC 垃圾回收看這一篇就夠了
GC垃圾回收
1. 如何判斷一個對象是否可以回收?
引用計數(shù)算法
給對象添加一個引用計數(shù)器,當(dāng)對象增加一個引用時計數(shù)器加 1,引用失效時計數(shù)器減 1。引用計數(shù)為 0 的對象可被回收。
兩個對象出現(xiàn)循環(huán)引用的情況下,此時引用計數(shù)器永遠(yuǎn)不為 0,導(dǎo)致無法對它們進行回收。
正因為循環(huán)引用的存在,因此 Java 虛擬機不使用引用計數(shù)算法。
可達性分析算法
通過 GC Roots 作為起始點進行搜索,能夠到達到的對象都是存活的,不可達的對象可被回收。

Java 虛擬機使用該算法來判斷對象是否可被回收,在 Java 中 GC Roots 一般包含以下內(nèi)容:
虛擬機棧中引用的對象
本地方法棧中引用的對象
方法區(qū)中類靜態(tài)屬性引用的對象
方法區(qū)中的常量引用的對象
2. 對象有哪些引用類型?
無論是通過引用計算算法判斷對象的引用數(shù)量,還是通過可達性分析算法判斷對象是否可達,判定對象是否可被回收都與引用有關(guān)。
Java 具有四種強度不同的引用類型。
強引用
被強引用關(guān)聯(lián)的對象不會被回收。
軟引用
被軟引用關(guān)聯(lián)的對象只有在內(nèi)存不夠的情況下才會被回收。
弱引用
被弱引用關(guān)聯(lián)的對象一定會被回收,也就是說它只能存活到下一次垃圾回收發(fā)生之前。
虛引用
又稱為幽靈引用或者幻影引用。一個對象是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用取得一個對象。
為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象被回收時收到一個系統(tǒng)通知。
3. 有哪些基本的垃圾回收算法?
標(biāo)記 - 清除

將存活的對象進行標(biāo)記,然后清理掉未被標(biāo)記的對象。
不足:
標(biāo)記和清除過程效率都不高;
會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,導(dǎo)致無法給大對象分配內(nèi)存。
標(biāo)記 - 整理

讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存。
復(fù)制

將內(nèi)存劃分為大小相等的兩塊,每次只使用其中一塊,當(dāng)這一塊內(nèi)存用完了就將還存活的對象復(fù)制到另一塊上面,然后再把使用過的內(nèi)存空間進行一次清理。
主要不足是只使用了內(nèi)存的一半。
現(xiàn)在的商業(yè)虛擬機都采用這種收集算法來回收新生代,但是并不是將新生代劃分為大小相等的兩塊,而是分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活著的對象一次性復(fù)制到另一塊 Survivor 空間上,最后清理 Eden 和使用過的那一塊 Survivor。
HotSpot 虛擬機的 Eden 和 Survivor 的大小比例默認(rèn)為 8:1,保證了內(nèi)存的利用率達到 90%。如果每次回收有多于 10% 的對象存活,那么一塊 Survivor 空間就不夠用了,此時需要依賴于老年代進行分配擔(dān)保,也就是借用老年代的空間存儲放不下的對象。
分代收集
現(xiàn)在的商業(yè)虛擬機采用分代收集算法,它根據(jù)對象存活周期將內(nèi)存劃分為幾塊,不同塊采用適當(dāng)?shù)氖占惴ā?/p>
一般將堆分為新生代和老年代。
新生代使用: 復(fù)制算法
老年代使用: 標(biāo)記 - 清除 或者 標(biāo)記 - 整理 算法
3.1 分代收集算法和分區(qū)收集算法區(qū)別?

分代收集算法
當(dāng)前主流 VM 垃圾收集都采用”分代收集”(Generational Collection)算法, 這種算法會根據(jù) 對象存活周期的不同將內(nèi)存劃分為幾塊, 如 JVM 中的 新生代、老年代、永久代,這樣就可以根據(jù) 各年代特點分別采用最適當(dāng)?shù)?GC 算法
在新生代-復(fù)制算法:
每次垃圾收集都能發(fā)現(xiàn)大批對象已死, 只有少量存活. 因此選用復(fù)制算法, 只需要付出少量 存活對象的復(fù)制成本就可以完成收集
在老年代-標(biāo)記整理算法:
因為對象存活率高、沒有額外空間對它進行分配擔(dān)保, 就必須采用“標(biāo)記—清理”或“標(biāo) 記—整理”算法來進行回收, 不必進行內(nèi)存復(fù)制, 且直接騰出空閑內(nèi)存.
ParNew: 一款多線程的收集器,采用復(fù)制算法,主要工作在 Young 區(qū),可以通過 -XX:ParallelGCThreads 參數(shù)來控制收集的線程數(shù),整個過程都是 STW 的,常與 CMS 組合使用。
CMS: 以獲取最短回收停頓時間為目標(biāo),采用“標(biāo)記-清除”算法,分 4 大步進行垃圾收集,其中初始標(biāo)記和重新標(biāo)記會 STW ,多數(shù)應(yīng)用于互聯(lián)網(wǎng)站或者 B/S 系統(tǒng)的服務(wù)器端上,JDK9 被標(biāo)記棄用,JDK14 被刪除。
分區(qū)收集算法
分區(qū)算法則將整個堆空間劃分為連續(xù)的不同小區(qū)間, 每個小區(qū)間獨立使用, 獨立回收. 這樣做的 好處是可以控制一次回收多少個小區(qū)間 , 根據(jù)目標(biāo)停頓時間, 每次合理地回收若干個小區(qū)間(而不是 整個堆), 從而減少一次 GC 所產(chǎn)生的停頓。
G1: 一種服務(wù)器端的垃圾收集器,應(yīng)用在多處理器和大容量內(nèi)存環(huán)境中,在實現(xiàn)高吞吐量的同時,盡可能地滿足垃圾收集暫停時間的要求。
ZGC: JDK11 中推出的一款低延遲垃圾回收器,適用于大內(nèi)存低延遲服務(wù)的內(nèi)存管理和回收,SPECjbb 2015 基準(zhǔn)測試,在 128G 的大堆下,最大停頓時間才 1.68 ms,停頓時間遠(yuǎn)勝于 G1 和 CMS。
什么是Minor GC、Major GC、Full GC?
JVM 在進行 GC 時,并非每次都對堆內(nèi)存(新生代、老年代;方法區(qū))區(qū)域一起回收的,大部分時候回收的都是指新生代。
針對 HotSpot VM 的實現(xiàn),它里面的 GC 按照回收區(qū)域又分為兩大類:部分收集(Partial GC),整堆收集(Full GC)
部分收集:不是完整收集整個 Java 堆的垃圾收集。其中又分為: * 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
老年代收集(Major GC/Old GC):只是老年代的垃圾收集 * 目前,只有 CMS GC 會有單獨收集老年代的行為
很多時候 Major GC 會和 Full GC 混合使用,需要具體分辨是老年代回收還是整堆回收
混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集 * 目前只有 G1 GC 會有這種行為
整堆收集(Full GC):收集整個 Java 堆和方法區(qū)的垃圾
說說JVM內(nèi)存分配策略?
對象優(yōu)先在 Eden 分配
大多數(shù)情況下,對象在新生代 Eden 區(qū)分配,當(dāng) Eden 區(qū)空間不夠時,發(fā)起 Minor GC。大對象直接進入老年代
大對象是指需要連續(xù)內(nèi)存空間的對象,最典型的大對象是那種很長的字符串以及數(shù)組。
經(jīng)常出現(xiàn)大對象會提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間分配給大對象。
-XX:PretenureSizeThreshold,大于此值的對象直接在老年代分配,避免在 Eden 區(qū)和 Survivor 區(qū)之間的大量內(nèi)存復(fù)制。
長期存活的對象進入老年代
為對象定義年齡計數(shù)器,對象在 Eden 出生并經(jīng)過 Minor GC 依然存活,將移動到 Survivor 中,年齡就增加 1 歲,增加到一定年齡則移動到老年代中。
-XX:MaxTenuringThreshold 用來定義年齡的閾值。
動態(tài)對象年齡判定
虛擬機并不是永遠(yuǎn)地要求對象的年齡必須達到 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 中相同年齡所有對象大小的總和大于 Survivor 空間的一半,則年齡大于或等于該年齡的對象可以直接進入老年代,無需等到 MaxTenuringThreshold 中要求的年齡。空間分配擔(dān)保
在發(fā)生 Minor GC 之前,虛擬機先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果條件成立的話,那么 Minor GC 可以確認(rèn)是安全的。
如果不成立的話虛擬機會查看 HandlePromotionFailure 設(shè)置值是否允許擔(dān)保失敗,如果允許那么就會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次 Minor GC;如果小于,或者 HandlePromotionFailure 設(shè)置不允許冒險,那么就要進行一次 Full GC。
什么情況下會觸發(fā)Full GC?
對于 Minor GC,其觸發(fā)條件非常簡單,當(dāng) Eden 空間滿時,就將觸發(fā)一次 Minor GC。而 Full GC 則相對復(fù)雜,有以下條件:
調(diào)用 System.gc()
只是建議虛擬機執(zhí)行 Full GC,但是虛擬機不一定真正去執(zhí)行。不建議使用這種方式,而是讓虛擬機管理內(nèi)存。老年代空間不足
老年代空間不足的常見場景為前文所講的大對象直接進入老年代、長期存活的對象進入老年代等。
為了避免以上原因引起的 Full GC,應(yīng)當(dāng)盡量不要創(chuàng)建過大的對象以及數(shù)組。除此之外,可以通過 -Xmn 虛擬機參數(shù)調(diào)大新生代的大小,讓對象盡量在新生代被回收掉,不進入老年代。還可以通過 -XX:MaxTenuringThreshold 調(diào)大對象進入老年代的年齡,讓對象在新生代多存活一段時間。
空間分配擔(dān)保失敗
使用復(fù)制算法的 Minor GC 需要老年代的內(nèi)存空間作擔(dān)保,如果擔(dān)保失敗會執(zhí)行一次 Full GC。JDK 1.7 及以前的永久代空間不足
在 JDK 1.7 及以前,HotSpot 虛擬機中的方法區(qū)是用永久代實現(xiàn)的,永久代中存放的為一些 Class 的信息、常量、靜態(tài)變量等數(shù)據(jù)。
當(dāng)系統(tǒng)中要加載的類、反射的類和調(diào)用的方法較多時,永久代可能會被占滿,在未配置為采用 CMS GC 的情況下也會執(zhí)行 Full GC。如果經(jīng)過 Full GC 仍然回收不了,那么虛擬機會拋出 java.lang.OutOfMemoryError。
為避免以上原因引起的 Full GC,可采用的方法為增大永久代空間或轉(zhuǎn)為使用 CMS GC。
Concurrent Mode Failure
執(zhí)行 CMS GC 的過程中同時有對象要放入老年代,而此時老年代空間不足(可能是 GC 過程中浮動垃圾過多導(dǎo)致暫時性的空間不足),便會報 Concurrent Mode Failure 錯誤,并觸發(fā) Full GC。
Hotspot中有哪些垃圾回收器?

以上是 HotSpot 虛擬機中的 7 個垃圾收集器,連線表示垃圾收集器可以配合使用。
單線程與多線程: 單線程指的是垃圾收集器只使用一個線程進行收集,而多線程使用多個線程;
串行與并行: 串行指的是垃圾收集器與用戶程序交替執(zhí)行,這意味著在執(zhí)行垃圾收集的時候需要停頓用戶程序;并形指的是垃圾收集器和用戶程序同時執(zhí)行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式執(zhí)行。
Serial 收集器

Serial 翻譯為串行,也就是說它以串行的方式執(zhí)行。
它是單線程的收集器,只會使用一個線程進行垃圾收集工作。
它的優(yōu)點是簡單高效,對于單個 CPU 環(huán)境來說,由于沒有線程交互的開銷,因此擁有最高的單線程收集效率。
它是 Client 模式下的默認(rèn)新生代收集器,因為在用戶的桌面應(yīng)用場景下,分配給虛擬機管理的內(nèi)存一般來說不會很大。Serial 收集器收集幾十兆甚至一兩百兆的新生代停頓時間可以控制在一百多毫秒以內(nèi),只要不是太頻繁,這點停頓是可以接受的。
ParNew 收集器

它是 Serial 收集器的多線程版本。
是 Server 模式下的虛擬機首選新生代收集器,除了性能原因外,主要是因為除了 Serial 收集器,只有它能與 CMS 收集器配合工作。
默認(rèn)開啟的線程數(shù)量與 CPU 數(shù)量相同,可以使用 -XX:ParallelGCThreads 參數(shù)來設(shè)置線程數(shù)。
Parallel Scavenge 收集器
與 ParNew 一樣是多線程收集器。
其它收集器關(guān)注點是盡可能縮短垃圾收集時用戶線程的停頓時間,而它的目標(biāo)是達到一個可控制的吞吐量,它被稱為“吞吐量優(yōu)先”收集器。這里的吞吐量指 CPU 用于運行用戶代碼的時間占總時間的比值。
停頓時間越短就越適合需要與用戶交互的程序,良好的響應(yīng)速度能提升用戶體驗。而高吞吐量則可以高效率地利用 CPU 時間,盡快完成程序的運算任務(wù),主要適合在后臺運算而不需要太多交互的任務(wù)。
縮短停頓時間是以犧牲吞吐量和新生代空間來換取的: 新生代空間變小,垃圾回收變得頻繁,導(dǎo)致吞吐量下降。
可以通過一個開關(guān)參數(shù)打開 GC 自適應(yīng)的調(diào)節(jié)策略(GC Ergonomics),就不需要手動指定新生代的大小(-Xmn)、Eden 和 Survivor 區(qū)的比例、晉升老年代對象年齡等細(xì)節(jié)參數(shù)了。虛擬機會根據(jù)當(dāng)前系統(tǒng)的運行情況收集性能監(jiān)控信息,動態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時間或者最大的吞吐量。
Serial Old 收集器

是 Serial 收集器的老年代版本,也是給 Client 模式下的虛擬機使用。如果用在 Server 模式下,它有兩大用途:
在 JDK 1.5 以及之前版本(Parallel Old 誕生以前)中與 Parallel Scavenge 收集器搭配使用。
作為 CMS 收集器的后備預(yù)案,在并發(fā)收集發(fā)生 Concurrent Mode Failure 時使用。
Parallel Old 收集器

是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 資源敏感的場合,都可以優(yōu)先考慮 Parallel Scavenge 加 Parallel Old 收集器。
CMS 收集器

CMS(Concurrent Mark Sweep),Mark Sweep 指的是標(biāo)記 - 清除算法。
分為以下四個流程:
初始標(biāo)記: 僅僅只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對象,速度很快,需要停頓。
并發(fā)標(biāo)記: 進行 GC Roots Tracing 的過程,它在整個回收過程中耗時最長,不需要停頓。
重新標(biāo)記: 為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄,需要停頓。
并發(fā)清除: 不需要停頓。
在整個過程中耗時最長的并發(fā)標(biāo)記和并發(fā)清除過程中,收集器線程都可以與用戶線程一起工作,不需要進行停頓。
具有以下缺點:
吞吐量低: 低停頓時間是以犧牲吞吐量為代價的,導(dǎo)致 CPU 利用率不夠高。
無法處理浮動垃圾,可能出現(xiàn) Concurrent Mode Failure。浮動垃圾是指并發(fā)清除階段由于用戶線程繼續(xù)運行而產(chǎn)生的垃圾,這部分垃圾只能到下一次 GC 時才能進行回收。由于浮動垃圾的存在,因此需要預(yù)留出一部分內(nèi)存,意味著 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。如果預(yù)留的內(nèi)存不夠存放浮動垃圾,就會出現(xiàn) Concurrent Mode Failure,這時虛擬機將臨時啟用 Serial Old 來替代 CMS。
標(biāo)記 - 清除算法導(dǎo)致的空間碎片,往往出現(xiàn)老年代空間剩余,但無法找到足夠大連續(xù)空間來分配當(dāng)前對象,不得不提前觸發(fā)一次 Full GC。
G1 收集器
G1(Garbage-First),它是一款面向服務(wù)端應(yīng)用的垃圾收集器,在多 CPU 和大內(nèi)存的場景下有很好的性能。HotSpot 開發(fā)團隊賦予它的使命是未來可以替換掉 CMS 收集器。
堆被分為新生代和老年代,其它收集器進行收集的范圍都是整個新生代或者老年代,而 G1 可以直接對新生代和老年代一起回收。

G1 把堆劃分成多個大小相等的獨立區(qū)域(Region),新生代和老年代不再物理隔離。

通過引入 Region 的概念,從而將原來的一整塊內(nèi)存空間劃分成多個的小空間,使得每個小空間可以單獨進行垃圾回收。這種劃分方法帶來了很大的靈活性,使得可預(yù)測的停頓時間模型成為可能。通過記錄每個 Region 垃圾回收時間以及回收所獲得的空間(這兩個值是通過過去回收的經(jīng)驗獲得),并維護一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的 Region。
每個 Region 都有一個 Remembered Set,用來記錄該 Region 對象的引用對象所在的 Region。通過使用 Remembered Set,在做可達性分析的時候就可以避免全堆掃描。

如果不計算維護 Remembered Set 的操作,G1 收集器的運作大致可劃分為以下幾個步驟:
初始標(biāo)記
并發(fā)標(biāo)記
最終標(biāo)記: 為了修正在并發(fā)標(biāo)記期間因用戶程序繼續(xù)運作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分標(biāo)記記錄,虛擬機將這段時間對象變化記錄在線程的 Remembered Set Logs 里面,最終標(biāo)記階段需要把 Remembered Set Logs 的數(shù)據(jù)合并到 Remembered Set 中。這階段需要停頓線程,但是可并行執(zhí)行。
篩選回收: 首先對各個 Region 中的回收價值和成本進行排序,根據(jù)用戶所期望的 GC 停頓時間來制定回收計劃。此階段其實也可以做到與用戶程序一起并發(fā)執(zhí)行,但是因為只回收一部分 Region,時間是用戶可控制的,而且停頓用戶線程將大幅度提高收集效率。
具備如下特點:
空間整合: 整體來看是基于“標(biāo)記 - 整理”算法實現(xiàn)的收集器,從局部(兩個 Region 之間)上來看是基于“復(fù)制”算法實現(xiàn)的,這意味著運行期間不會產(chǎn)生內(nèi)存空間碎片。
可預(yù)測的停頓: 能讓使用者明確指定在一個長度為 M 毫秒的時間片段內(nèi),消耗在 GC 上的時間不得超過 N 毫秒。
干貨分享
關(guān)注VX【頂尖架構(gòu)師棧】,回復(fù)?C01?獲取超10G后端學(xué)習(xí)面試資源

本文使用?文章同步助手?同步