如何確定垃圾
對堆垃圾回收前的第一步就是要判斷哪些對象已經死亡(即不能再被任何途徑使用的對象)
引用計數法
這個方法就是為對象添加計數器來標識引用個數,計數器為 0 的對象就是不可能再被使用的。但是這種方法存在循環引用問題,目前并未被使用。
?可達性分析算法
這個算法的基本思想就是通過一系列的稱為 “GC Roots” 的對象作為起點,從這些節點開始向下搜索,節點所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的,需要被回收。
那么關鍵是明確哪些是GC Roots:(咱們從頭到尾說一次Java垃圾回收 | Java程序員進階之路)
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中 JNI(即一般說的 Native 方法)引用的對象
垃圾回收算法
那么知道了內存中哪些對象是垃圾對象,怎么回收呢
標記-清除算法
顧名思義,就是標記垃圾對象,然后清除
?復制算法
它可以將內存分為大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完后,就將還存活的對象復制到另一塊去,然后再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。
?問題:
實際使用的空間只有一半; 復制操作會帶來開銷;
標記-整理算法
仍然是先標記,然后會讓所有存活的對象向一端移動,然后直接清理掉端邊界以外的內存。
?分代收集算法
只是根據對象存活周期的不同將內存分為幾塊。一般將 Java 堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。
比如在新生代中,每次收集都會有大量對象死去,所以可以選擇”標記-復制“算法,只需要付出少量對象的復制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇“標記-清除”或“標記-整理”算法進行垃圾收集。
垃圾回收器
上面介紹的都是理論算法,實際jvm會實現很多垃圾回收器,以供不同場合使用。
JDK 默認垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version 命令查看):
JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
JDK 9 ~ JDK20: G1
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它非常符合在注重用戶體驗的應用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虛擬機第一款真正意義上的并發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。
從名字中的Mark Sweep這兩個詞可以看出,CMS 收集器是一種 “標記-清除”算法實現的,它的運作過程相比于前面幾種垃圾收集器來說更加復雜一些。整個過程分為四個步驟:
初始標記: 暫停所有的其他線程,并記錄下直接與 root 相連的對象,速度很快 ;
并發標記: 同時開啟 GC 和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構并不能保證包含當前所有的可達對象。因為用戶線程可能會不斷的更新引用域,所以 GC 線程無法保證可達性分析的實時性。所以這個算法里會跟蹤記錄這些發生引用更新的地方。
重新標記: 重新標記階段就是為了修正并發標記期間因為用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比并發標記階段時間短
并發清除: 開啟用戶線程,同時 GC 線程開始對未標記的區域做清掃。
?
從它的名字就可以看出它是一款優秀的垃圾收集器,主要優點:并發收集、低停頓。但是它有下面三個明顯的缺點:
對 CPU 資源敏感;
無法處理浮動垃圾;
它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。
?
G1 收集器
G1 (Garbage-First) 是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足 GC 停頓時間要求的同時,還具備高吞吐量性能特征.
被視為 JDK1.7 中 HotSpot 虛擬機的一個重要進化特征。它具備以下特點:
- 并行與并發:G1 能充分利用 CPU、多核環境下的硬件優勢,使用多個 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓時間。部分其他收集器原本需要停頓 Java 線程執行的 GC 動作,G1 收集器仍然可以通過并發的方式讓 java 程序繼續執行。
- 分代收集:雖然 G1 可以不需要其他收集器配合就能獨立管理整個 GC 堆,但是還是保留了分代的概念。
- 空間整合:與 CMS 的“標記-清除”算法不同,G1 從整體來看是基于“標記-整理”算法實現的收集器;從局部上來看是基于“標記-復制”算法實現的。
- 可預測的停頓:這是 G1 相對于 CMS 的另一個大優勢,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內,消耗在垃圾收集上的時間不得超過 N 毫秒。
G1 收集器的運作大致分為以下幾個步驟:
- 初始標記
- 并發標記
- 最終標記
- 篩選回收
G1 收集器
G1 收集器在后臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來) 。這種使用 Region 劃分內存空間以及有優先級的區域回收方式,保證了 G1 收集器在有限時間內可以盡可能高的收集效率(把內存化整為零)。
?
內存分配和回收的原則
對象優先在 Eden 區分配
大部分對象都是“朝生晚死”,大多數情況下,對象會在新生代 Eden 區中進行分配。當 Eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC。
長期存活的對象將進入老年代
既然虛擬機采用了分代收集的思想來管理內存,那么內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代中。為了做到這一點,虛擬機給每個對象一個對象年齡(Age)計數器。
大部分情況,對象都會首先在 Eden 區域分配。如果對象在 Eden 出生并經過第一次 Minor GC 后仍然能夠存活,并且能被 Survivor 容納的話,將被移動到 Survivor 空間(s0 或者 s1)中,并將對象年齡設為 1(Eden 區->Survivor 區后對象的初始年齡變為 1)。
對象在 Survivor 中每熬過一次 MinorGC,年齡就增加 1 歲,當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 來設置。
大對象進入老年代
大對象就是需要大量連續內存空間的對象(比如:字符串、數組)。
大對象直接進入老年代的行為是由虛擬機動態決定的,它與具體使用的垃圾回收器和相關參數有關。大對象直接進入老年代是一種優化策略,旨在避免將大對象放入新生代,從而減少新生代的垃圾回收頻率和成本。
- G1垃圾回收器會根據-XX:G1HeapRegionSize參數設置的堆區域大小和-XX:G1MixedGCLiveThresholdPercent參數設置的閾值,來決定哪些對象會直接進入老年代。
- Parallel Scavenge垃圾回收器中,默認情況下,并沒有一個固定的閾值(XX:ThresholdTolerance是動態調整的)來決定何時直接在老年代分配大對象。而是由虛擬機根據當前的堆內存情況和歷史數據動態決定。
常見面試題:
- 如何判斷對象是否死亡(兩種方法)。
- 簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區別、使用軟引用能帶來的好處)。
- 如何判斷一個常量是廢棄常量
- 如何判斷一個類是無用的類
- 垃圾收集有哪些算法,各自的特點?
- HotSpot 為什么要分為新生代和老年代?
- 常見的垃圾回收器有哪些?
- 介紹一下 CMS,G1 收集器。
- Minor Gc 和 Full GC 有什么不同呢?