JVM 垃圾回收演算法和垃圾回收器

IT王小二發表於2021-06-12

JVM 垃圾回收演算法和垃圾回收器。

一、垃圾回收的區域

  • 棧:棧中的生命週期是跟隨執行緒,所以一般不需要關注。
  • 堆:堆中的物件是垃圾回收的重點。
  • 方法區:這一塊也會發生垃圾回收,不過這塊的效率比較低,一般不是我們關注的重點。

二、怎麼判斷物件的存活

一般有兩種方式(引用計數法、可達性分析),JVM使用的是可達性分析

1. 引用計數法

給物件新增一個引用計數器,當物件增加一個引用時計數器加 1,引用失效時計數器減 1。引用計數為 0 的物件可被回收(Python 在用,但主流虛擬機器沒有使用)。

  • 優點:快,方便,實現簡單。
  • 缺陷:物件相互引用時(A.instance=B 同時 B.instance=A),很難判斷物件是否該回收。

2. 可達性分析

來判定物件是否存活的。這個演算法的基本思路就是通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到 GC Roots 沒有任何引用鏈相連時,則證明此物件是不可用的。

作為 GC Roots 的物件包括下面幾種:

  • 當前虛擬機器棧中區域性變數表中的引用的物件
  • 方法區中類靜態屬性引用的物件
  • 方法區中的常量引用的物件

3. finalize

Java提供finalize()方法,垃圾回收器準備釋放記憶體的時候,會先呼叫finalize(),可以完成物件的拯救(不被回收),但是不能保證一定不被回收,說白了就是沒啥用,一個坑。

三、各種引用(Reference)

Reference 中儲存的資料代表的是另一塊記憶體的起始地址。

1. 強引用

一般的 Object obj = new Object() ,就屬於強引用。

(如果有 GCroots 的強引用)垃圾回收器絕對不會回收它,當記憶體不足時寧願丟擲 OOM 錯誤,使得程式異常停止,也不會回收強引用物件。

2. 軟引用

SoftReference垃圾回收器在記憶體充足的時候不會回收它,而在記憶體不足時會回收它。

示例程式碼:

public static void main(String[] args) {
    String str = new String("SunnyBear"); // 強引用
    SoftReference<String> strSoft = new SoftReference<String>(str);
    str = null; // 幹掉強引用,確保只有strSoft的軟引用
    System.out.println(strSoft.get()); // SunnyBear
    System.gc(); // 執行一次gc,此命令請勿線上上使用,僅作示例操作
    System.out.println("------------ gc after");
    System.out.println(str); // null
    System.out.println(strSoft.get()); // SunnyBear
}

所以軟引用一般用來實現一些記憶體敏感的快取,只要記憶體空間足夠,物件就會保持不被回收掉

3. 弱引用 WeakReference

垃圾回收器在掃描到該物件時,無論記憶體充足與否,都會回收該物件的記憶體

示例程式碼:

public static void main(String[] args) {
    String str = new String("SunnyBear"); // 強引用
    WeakReference<String> strWeak = new WeakReference<String>(str);
    str = null; // 幹掉強引用,確保只有strSoft的軟引用
    System.out.println(strWeak.get()); // SunnyBear
    System.gc(); // 執行一次gc,此命令請勿線上上使用,僅作示例操作
    System.out.println("------------ gc after"); // null
    System.out.println(str); // null
    System.out.println(strWeak.get()); // null
}

實際應用,如 WeakHashMap、ThreadLocal。

4. 虛引用 PhantomReference

幽靈引用,最弱,被垃圾回收的時候收到一個通知,如果一個物件只具有虛引用,那麼它和沒有任何引用一樣,任何時候都可能被回收。

虛引用主要用來跟蹤物件被垃圾回收器回收的活動。

四、GC

1. Minor GC

  • 特點: 發生在新生代上,發生的較頻繁,執行速度較快。
  • 觸發條件: Eden 區空間不足/空間分配擔保。

2. Full GC

  • 特點:主要發生在老年代上(新生代也會回收),較少發生,執行速度較慢。
  • 觸發條件:
    • 呼叫 System.gc() 。
    • 老年代區域空間不足。
    • 空間分配擔保失敗。
    • JDK 1.7 及以前的永久代(方法區)空間不足。

五、垃圾回收演算法

1. 複製演算法(Copying)

將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要按順序分配記憶體即可,實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小為了原來的一半。

複製演算法

  • 優點
    • 簡單高效,不會出現記憶體碎片。
  • 缺點
    • 記憶體利用率低。
    • 存活物件較多時效率明顯降低,因為需要移動每個不可回收資料的記憶體實際位置。

注:

專門研究表明,新生代中的物件 90%是“朝生夕死”的,所以一般來說回收佔據 10% 的空間夠用了,所以並不需要按照 1:1 的比例來劃分記憶體空間,而是將記憶體分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor[1]。當回收時,將 Eden 和 Survivor 中還存活著的物件一次性地複製到另外一塊 Survivor 空間上,最後清理掉 Eden 和剛才用過的 Survivor 空間。
HotSpot 虛擬機器預設 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的 90%(80%+10%),只有 10%的記憶體會被“浪費”。

2. 標記-清除演算法(Mark-Sweep)

首先標記所有需要回收的物件,然後統一回收被標記的物件。

標記清除演算法

  • 優點
    • 利用率100% 。
  • 缺點
    • 標記和清除效率都不高(對比複製演算法)。
    • 會產生大量不連續的記憶體碎片。

3. 標記-整理演算法(Mark-compact)

首先標記出所有需要回收的物件,在標記完成後,後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端,邊界以外的記憶體。

標記整理演算法

  • 優點
    • 利用率100% 。
    • 沒有記憶體碎片。
  • 缺點
    • 標記和清除效率都不高(對比複製演算法及標記清楚演算法)。

六、垃圾回收器

jvm 垃圾回收器把上面的三種演算法全部用到了,採用分代收集。

1、新生代:複製演算法。

收集器 收集物件和演算法 收集器型別
Serial 新生代,複製演算法 單執行緒
ParNew 新生代,複製演算法 並行的多執行緒收集器
Parallel Scavenge 新生代,複製演算法 並行的多執行緒收集器

2、老年代:標記清除演算法和標記整理演算法

收集器 收集物件和演算法 收集器型別
Serial Old 老年代,標記整理演算法 單執行緒
Parallel Old 老年代,標記整理演算法 並行的多執行緒收集器
CMS(Conc Mark Sweep ) 老年代,標記清除演算法 並行和併發收集器
G1(Garbage First) 跨新生代和老年代,複製演算法 + 標記整理演算法 並行和併發收集器

注:

  • 並行:垃圾收集的多執行緒的同時進行。
  • 併發:垃圾收集的多執行緒和使用者應用的多執行緒同時進行。
  • 使用 jps -v 可以看到使用的垃圾收集器,例如:-XX:+UseConcMarkSweepGC (CMS)

1. 可以配套使用的垃圾回收器

連線表示可以 新生代老年代 配套使用的垃圾收集器。

垃圾回收器

2. Serial/Serial Old

最古老的,單執行緒,獨佔式,成熟,適合單 CPU 伺服器。-XX:+UseSerialGC 新生代和老年代都用序列收集器。

3. ParNew

ParNew 和 Serial 基本沒區別,唯一的區別:多執行緒,多 CPU 的,停頓時間比 Serial 少 。

-XX:+UseParNewGC 新生代使用 ParNew,老年代使用 Serial Old 。

可以和CMS搭配使用。

4. Parallel Scavenge(ParallerGC)/Parallel Old

關注吞吐量的垃圾收集器,高吞吐量則可以高效率地利用 CPU 時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務。所謂吞吐量就是 CPU 用於執行使用者程式碼的時間與 CPU 總消耗時間的比值,即吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間),虛擬機器總共執行了 100 分鐘,其中垃圾收集花掉 1 分鐘,那有吞吐效率就是 99% 。

5. CMS(Concurrent Mark Sweep)

收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的 Java 應用集中在網際網路站或者 B/S 系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。

CMS 收集器就非常符合這類應用的需求。-XX:+UseConcMarkSweepGC一般新生代使用 ParNew,老年代的用 CMS,從名字(包含“Mark Sweep”)上就可以看出,CMS 收集器是基於“標記—清除”演算法實現的,它的運作過程相對於前面幾種收集器來說更復雜一些。

回收過程

CMS回收過程

整個過程分為 4 個步驟,包括:

1、初始標記:僅僅只是標記一下 GC Roots 能直接關聯到的物件,速度很快,需要停頓(STW -Stop the world)。

2、併發標記:從 GC Root 開始對堆中物件進行可達性分析,找到存活物件,它在整個回收過程中耗時最長,不需要停頓。

3、重新標記:為了修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,需要停頓(STW)。這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。

  1. 併發清除:不需要停頓。

優缺點

1、優點

由於整個過程中耗時最長的併發標記和併發清除過程收集器執行緒都可以與使用者執行緒一起工作,所以,從總體上來說,CMS 收集器的記憶體回收過程是與使用者執行緒一起併發執行的

2、缺點

  • CPU 資源敏感:因為併發階段多執行緒佔據 CPU 資源,如果 CPU 資源不足,效率會明顯降低。
  • 由於 CMS 併發清理階段 使用者執行緒還在執行著,伴隨程式執行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS 無法在當次收集中處理掉它們,只好留待下一次 GC 時再清理掉。這一部分垃圾就稱為 浮動垃圾
  • 由於浮動垃圾的存在,因此需要預留出一部分記憶體,意味著 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。在 1.6 的版本中老年代空間使用率閾值(92%),如果預留的記憶體不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛擬機器將臨時啟用 Serial Old 來替代 CMS。
  • 會產生記憶體碎片:標記-清除演算法 會導致產生不連續的記憶體碎片。

6. G1

G1相比較CMS的改進

  • 基於標記-整理演算法, 不會產生空間碎片,分配大物件時不會無法得到連續的空間而提前觸發一次full gc 。
  • 停頓時間可控: G1可以通過設定預期停頓時間(Pause time)來控制垃圾收集時間,但是這個預期停頓時間G1只能儘量做到,而不是一定能做到

可預測的停頓:

G1 收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個 Java 堆中進行全區域的垃圾收集。G1 跟蹤各個 Region 裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的 Region(這也就是 Garbage-First 名稱的來由)。這種使用 Region 劃分記憶體空間以及有優先順序的區域回收方式,保證了 G1 收集器在有限的時間內可以獲取儘可高的收集效率。

G1的設定引數

  • -XX:+UseG1GC // 開啟G1
  • -XX:MaxGCPauseMillis=200 // 預期停頓時間200毫秒,預設也是200
  • -XX:G1HeapRegionSize=2 // 設定每個區域大小2M,其必須是2的冪,範圍允許為1Mb到32Mb
  • -XX:G1NewSizePercent // 新生代最小值,預設值 5%
  • -XX:G1MaxNewSizePercent // 新生代最大值,預設值 60%
  • -XX:ParallelGCThreads // STW 期間,並行 GC 執行緒數
  • -XX:ConcGCThreads=n // 併發標記階段,並行執行的執行緒數

G1是怎麼劃堆記憶體的呢

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

G1 演算法將堆劃分為若干個獨立區域(Region),它仍然屬於分代收集器。不過,這些區域的一部分包含新生代,新生代的垃圾收集依然採用暫停所有應用執行緒的方式,將存活物件拷貝到老年代或者 Survivor 空間。例如其中一個獨立區域如圖:

獨立區域

GC 的模式

1、Young GC

Young GC 主要是對 Eden 區進行 GC,它在 Eden 空間耗盡時會被觸發。在這種情況下,Eden 空間的資料移動到 Survivor 空間中,如果 Survivor 空間不夠,Eden 空間的部分資料會直接晉升到老年代空間。Survivor 區的資料移動到新的 Survivor 區中,也有部分資料晉升到老年代空間中。最終 Eden 空間的資料為空,GC 停止工作,應用執行緒繼續執行

2、Mixed GC

選定所有新生代裡的 Region,外加根據 global concurrent marking 統計得出收集收益高的若干老年代 Region。在使用者指定的開銷目標範圍內儘可能選擇益高的老年代 Region。Mixed GC 不是 full GC,它只能回收部分老年代的 Region。如果 mixed GC 實在無法跟上程式分配記憶體的速度,導致老年代填滿無法繼續進行 Mixed GC,就會使用 serial old GC(full GC)來收集整個 GC heap。所以我們可以知道,G1 是不提供 full GC 的

收集過程

G1回收過程

大致分為4個步驟:

1、初始標記:僅僅只是標記一下 GC Roots 能直接關聯到的物件,並且修改 TAMS(Nest Top Mark Start)的值,讓下一階段使用者程式併發執行時,能在正確可以的 Region 中建立物件,此階段需要停頓執行緒(STW),但耗時很短

2、併發標記:從 GC Root 開始對堆中物件進行可達性分析,找到存活物件,此階段耗時較長,但可與使用者程式併發執行

3、最終標記:為了修正在併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機器將這段時間物件變化記錄線上程的 Remembered Set Logs 裡面,最終標記階段需要把 Remembered Set Logs 的資料合併到 Remembered Set 中。這階段需要停頓執行緒(STW),但是可並行執行

4、篩選回收:首先對各個 Region 中的回收價值和成本進行排序,根據使用者所期望的 GC 停頓時間來制定回收計劃。此階段其實也可以做到與使用者程式一起併發執行,但是因為只回收一部分 Region,時間是使用者可控制的,而且停頓使用者執行緒將大幅度提高收集效率

七、垃圾回收器的一些重要引數

引數 描述
UseSerialGC 虛擬機器執行在 Client 模式下的預設值,開啟此開關後,使用 Serial+Serial Old 的收集器組合進行記憶體回收
UseParNewGC 開啟此開關後,使用 ParNew + Serial Old 的收集器組合進行記憶體回收
UseConcMarkSweepGC 開啟此開關後,使用 ParNew + CMS + Serial Old 的收集器組合進行記憶體回收。Serial Old 收集器將作為 CMS 收集器出現 Concurrent Mode Failure 失敗後的後備收集器使用
UseParallelGC 虛擬機器執行在 Server 模式下的預設值,開啟此開關後,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器組合進行記憶體回收
UseParallelOldGC 開啟此開關後,使用 Parallel Scavenge + Parallel Old 的收集器組合進行記憶體回收
SurvivorRatio 新生代中 Eden 區域與 Survivor 區域的容量比值,預設為 8,代表 Eden : Survivor = 8 : 1
PretenureSizeThreshold 直接晉升到老年代的物件大小,設定這個引數後,大於這個引數的物件將直接在老年代分配
MaxTenuringThreshold 晉升到老年代的物件年齡,每個物件在堅持過一次 Minor GC 之後,年齡就增加 1,當超過這個引數值時就進入老年代
UseAdaptiveSizePolicy 動態調整 Java 堆中各個區域的大小以及進入老年代的年齡
HandlePromotionFailure 是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代的整個 Eden 和 Survivor 區的所有物件都存活的極端情況
ParallelGCThreads 設定並行 GC 時進行記憶體回收的執行緒數
GCTimeRatio GC 時間佔總時間的比率,預設值為 99,即允許 1% 的 GC 時間,僅在使用 Parallel Scavenge 收集器生效
MaxGCPauseMillis 設定 GC 的最大停頓時間,僅在使用 Parallel Scavenge 收集器時生效
CMSInitiatingOccupancyFraction 設定 CMS 收集器在老年代空間被使用多少後觸發垃圾收集,預設值為 68%,僅在使用 CMS 收集器時生效
UseCMSCompactAtFullCollection 設定 CMS 收集器在完成垃圾收集後是否要進行一次記憶體碎片整理,僅在使用 CMS 收集器時生效
CMSFullGCsBeforeCompaction 設定 CMS 收集器在進行若干次垃圾收集後再啟動一次記憶體碎片整理,僅在使用 CMS 收集器時生效

都讀到這裡了,來個 點贊、評論、關注、收藏 吧!

文章作者:IT王小二
首發地址:https://www.itwxe.com/posts/a4932e00/
版權宣告:文章內容遵循 署名-非商業性使用-禁止演繹 4.0 國際 進行許可,轉載請在文章頁面明顯位置給出作者與原文連結。

相關文章