JVM 垃圾回收算法&垃圾收集器

對象是否可回收判斷方式

引用計數法

對象每被引用一次,其引用計數就+1,不再引用時就-1,這樣雖然簡單高效,但是無法解決互相引用的問題,比如A持有B,B持有A A a=new A(); B b =new B(); a.instance=b; b.instance=a; a=null;b=null; 這樣引用計數都不爲0,也就無法回收

 

可達性分析

確定一批GC Roots引用,然後以這些引用爲出發點一級級的查找可以引用到的對象,但凡不能被引用到的對象,就屬於廢對象,搜索經過的路徑稱爲引用鏈

哪些對象可以作爲GC Roots呢?

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 方法區中的類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中引用的對象

 

垃圾回收算法

標記-清除算法(Mark-Sweep)


這是最基礎的收集算法,如它的名字一樣,算法分爲“標記”和“清除”兩個階段:

首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象。

之所以說它是最基礎的收集算法,是因爲後續的收集算法都是基於這種思路並對其缺點進行改進而得到的。

它的主要缺點有兩個:

  • 一個是效率問題,標記和清除過程的效率都不高;
  • 另外一個是空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致,當程序在以後的運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

 

複製算法(Copying)


爲了解決效率問題,一種稱爲“複製”(Copying)的收集算法出現了,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。

這樣使得每次都是對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半,未免太高了一點。

但是這種算法的效率相當高,所以,現在的商業虛擬機都採用這種收集算法來回收新生代。爲什麼新生代可以使用複製算法呢?

IBM 有專門研究表明,新生代中的對象 98% 都是朝生夕死,所以就不需要按照1:1的比例來劃分內存空間。這裏鑑於此,新生代採用瞭如下的劃分策略。

現在把新生代再劃分爲三部分,一塊較大的 Eden(伊甸園) 和兩塊較小的 Survivor(倖存者) 區域。

當回收時,將 Eden 和 Survivor 中還存活着的對象一次性地拷貝到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor的空間。HotSpot 虛擬機默認Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用內存空間爲整個新生代容量的90%(80%+10%),只有10%的內存是會被“浪費”的。

這樣清理完成後,原來的 Survivor 就空了,並一直保持爲空,直到下次 Minor GC 時,它再作爲存活對象的盛放地。兩個 Survivor 就這樣輪流當做 GC 過程中新生代存活對象的中轉站。

但是,如果使用複製算法的內存區域有大量的存活對象時,複製算法就會變得捉襟見肘,這時需要更大的 Survivor 區用於盛放那些存活對象,甚至可能需要 1:1的比例。所以針對堆內存區域的老年代,就有了下面的算法。

 

標記-整理算法


標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。這種方法避免了碎片的產生,同時也不需要一塊額外的內存空間,對於老年代會比較合適。

但是相比複製算法,雖然該算法佔用的內存空間少,但是耗費的垃圾回收時間會比複製算法久,所以上面也說了

我們應該儘量避免或者減少 Full GC 的發生。

這兩種算法用精煉的語言描述就是

  • 複製算法:用空間換時間

  • 標記-整理算法:用時間換空間

一句話 魚與熊掌不可兼得,但是針對新生代和老年代,他們都是最佳的選擇。

 

分代回收算法

一個應用啓動,操作系統會給他分配一個初始的內存大小,由上可知,這部分內存大部分應該屬於堆內存,JVM 爲了更好地利用管理這部分內存,對該區域做了劃分。一部分成爲新生代,另一部分稱爲老年代。

一開始對象的創建都發生在新生代,隨着對象的不斷創建,如果新生代沒有空間創建新對象,將會發生 GC ,這時的 GC 稱之爲 Minor GC,位於新生代的對象每經過一次 Minor GC 後,如果這個對象沒有被回收,則爲自己的標記數加1,這個標記數用於標識這個對象經歷了多少次的 Minor GC,對於 Sun 的 Hotspot 虛擬機,如果這個次數超過 15 ,該對象纔會被移動到老年代。

隨着時間的推移,如果老年代也沒有足夠的空間容納對象,老年代也會試着發起 GC,這時的 GC 被稱爲 Full GC

相比 Minor GC,Full GC 發生的次數比較少,但是每發生一次 Full GC,整個堆內存區域都需要執行一次垃圾回收,這對程序性能造成的影響比 Minor GC 大很多。所以我們應該儘量避免或者減少 Full GC 的發生

同時,在堆內存區域,發生最多的 GC 情形就是新生代的 Minor GC 了,因爲所有的對象會優先去新生代開闢空間,所以這塊的內存變化會很快,只有內存不夠用,就會發生 GC,但是一般的 Minor GC 執行比 Full GC 快很多。爲什麼呢?因爲新生代和老年代的垃圾回收算法不一樣。

這個算法並沒有什麼新鮮的東西,只是根據java對象的生存規律(大部分對象都是朝生夕死)將堆分爲新生代和老年代,對象都在新生代中創建,大部分也會在新生代中被回收,而留下的大年齡對象都被轉移到老年代中,對於新生代,由於存活率很低,所以適合採用複製法,而老年代對象存活率高,採用標記清除法或者標記移動法進行回收

 

小結

簡單梳理一下文中講到的一些知識點

  • 爲了更好的管理堆內存,該區域分爲新生代和老年代。

  • 新生代發生垃圾回收要比老年代頻繁。

  • 新生代發生的垃圾回收成爲 Minor GC;老年代發生的 GC 成爲 Full GC。

  • 新生代使用複製算法進行垃圾回收;老年代使用標記-整理算法

  • 爲了更高效管理新生代的內存,按照複製算法,結合 IBM 的研究論證,新生代分爲三塊,一塊比較大的 Eden 區和兩塊比較小的 Survivor 區,比例爲 8:1:1

 

垃圾收集器

基本概念

串行、並行和併發

計算機系統的信息交換有兩種方式:並行數據傳輸方式和串行數據傳輸方式。

  • 串行: 計算機中的串行是用 Serial 表示。A 和 B 兩個任務運行在一個 CPU 線程上,在 A 任務執行完之前不可以執行 B。即,在整個程序的運行過程中,僅存在一個運行上下文,即一個調用棧一個堆。程序會按順序執行每個指令。

  • 並行: 並行性指兩個或兩個以上事件或活動在同一時刻發生。在多道程序環境下,並行性使多個程序同一時刻可在不同 CPU 上同時執行。比如,A 和 B 兩個任務可以同時運行在不同的 CPU 線程上,效率較高,但受限於 CPU 線程數,如果任務數量超過了 CPU 線程數,那麼每個線程上的任務仍然是順序執行的。

  • 併發: 併發指多個線程在宏觀(相對於較長的時間區間而言)上表現爲同時執行,而實際上是輪流穿插着執行,併發的實質是一個物理 CPU 在若干道程序之間多路複用,其目的是提高有限物理資源的運行效率。 併發與並行串行並不是互斥的概念,如果是在一個CPU線程上啓用併發,那麼自然就還是串行的,而如果在多個線程上啓用併發,那麼程序的執行就可以是既併發又並行的。

在這裏插入圖片描述

JVM 垃圾收集中的串行、並行和併發

在 JVM 垃圾收集器中也涉及到如上的三個概念。

  • 串行(Serial): 使用單線程進行垃圾回收的回收器。

  • 並行(Parallel): 指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。

  • 併發(Concurrent): 指用戶線程與垃圾收集線程同時執行(但不一定是並行,可能會交替執行),用戶程序在繼續運行,而垃圾收集器運行在另一個 CPU 上。

在瞭解了這些概念之後,我們開始具體介紹常用的垃圾收集器。

 

串行垃圾回收器

如上所述,串行回收器是指使用單線程進行垃圾回收的回收器,每次回收時串行回收器只有一個工作線程,對於併發能力較弱的計算機來說,串行回收器的專注性和獨佔性往往有更好的表現。串行回收器可以在新生代和老年代使用,根據作用的堆空間不同,分爲新生代串行回收器和老年代串行回收器。

Serial

Serial收集器是最古老的收集器,它的缺點是當Serial收集器想進行垃圾回收的時候,必須暫停用戶的所有進程,即 STW(Stop The World,服務暫停)。到現在爲止,它依然是虛擬機運行在 client 模式下的默認新生代收集器。

參數控制:-XX:+UseSerialGC 使用串行收集器。

Serial Old

Serial 收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在 JDK1.5 以及以前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途是作爲 CMS 收集器的後備方案。

UseSerialGC:開啓此參數使用 Serial & Serial Old 蒐集器(client 模式默認值)。

 

並行垃圾回收器

並行回收器是在串行回收器的基礎上做了改進,它可以使用多個線程同時進行垃圾回收,對於計算能力強的計算機來說,可以有效的縮短垃圾回收所需的實際時間。

ParNew

ParNew 收集器是一個工作在新生代的垃圾收集器,它只是簡單的將串行收集器多線程化,它的回收策略和算法和串行回收器一樣。新生代並行,老年代串行;新生代複製算法、老年代標記-整理。

參數控制:-XX:+UseParNewGC 使用 ParNew 收集器;-XX:ParallelGCThreads 限制線程數量

除了 Serial 收集器外,只有它能與 CMS 收集器(真正意義上的併發收集器,後面會介紹到)配合工作。

Parallel

Parallel 是採用複製算法的多線程新生代垃圾回收器,Parallel 收集器更關注系統的吞吐量。所謂吞吐量就是 CPU 用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間/(運行用戶代碼時間 + 垃圾收集時間)

停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能夠提升用戶的體驗;

而高吞吐量則可以最高效率地利用CPU時間,儘快地完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務。

可以通過參數來打開自適應調節策略,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或最大的吞吐量;也可以通過參數控制GC的時間不大於多少毫秒或者比例;新生代複製算法、老年代標記-整理

參數控制:

  • -XX:MaxGCPauseMillis: 設置最大垃圾收集停頓時間

  • -XX:GCTimeRatio: 設置吞吐量的大小(默認是99)

  • -XX:+UseAdaptiveSeizPolicy: 打開自適應模式,當這個參數打開之後,就不需要手工指定新生代的大小、Eden與Survivor區的比例、晉升老年代對象年齡等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量

Parallel Old

Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,採用多線程和標記-整理算法,也是比較關注吞吐量。在注重吞吐量及 CPU 資源敏感的場合,都可以優先考慮 Parallel 加 Parallel Old 收集器。

參數控制:-XX:+UseParallelOldGC 使用 Parallel Old 收集器;-XX:ParallelGCThreads 限制線程數量。

CMS 垃圾回收器

CMS(Concurrent Mark Sweep)併發標記請除,它使用的是標記-清除法,工作在老年代,主要關注系統的停頓時間。

CMS 並不是獨佔的回收器,也就是說,CMS 回收的過程中應用程序仍然在不停的工作,又會有新的垃圾不斷的產生,所以在使用CMS的過程中應該確保應用程序的內存足夠可用,CMS不會等到應用程序飽和的時候纔去回收垃圾,而是在某一閥值(默認爲68)的時候開始回收,也就是說當老年代的空間使用率達到68%的時候會執行CMS。如果內存使用率增長很快,在CMS執行過程中,已經出現了內存不足的情況,此時,CMS回收就會失敗,虛擬機將啓動老年代 Serial 進行垃圾回收,這會導致應用程序中斷,直到垃圾回收完成後纔會正常工作,這個過程GC的停頓時間可能較長,所以閥值的設置要根據實際情況設置。

  • 初始標記: 暫停所有的其他線程,並記錄下直接與root相連的對象,速度很快;

  • 併發標記: 同時開啓GC和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構並不能保證包含當前所有的可達對象。因爲用戶線程可能會不斷的更新引用域,GC線程無法保證可達性分析的實時性。所以這個算法裏會跟蹤記錄這些發生引用更新的地方。

  • 重新標記: 重新標記階段就是爲了修正併發標記期間因爲用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短

  • 併發清除: 開啓用戶線程,同時GC線程開始對未標記的區域做清掃。

主要優點: 併發收集、低停頓。

但是它有下面三個明顯的缺點:

  • 對CPU資源敏感

  • 無法處理浮動垃圾

  • 使用的 標記-清除 算法會導致收集結束時會有大量空間碎片產生

標記清除法產生的內存碎片問題,CMS 提供提供了一些優化設置,可以設置完成 CMS 之後進行一次碎片整理,也可以設置進行多少次 CMS 回收後進行碎片整理。

參數控制:

  • -XX:+UserConcMarkSweepGC: 使用 CMS 垃圾清理器

  • -XX:CMSInitatingPermOccupancyFraction: 設置閥值

  • -XX:ConcGCThreads: 限制線程數量

  • -XX:+UseCMSCompactAtFullCollection: 設置完成 CMS 之後進行一次碎片整理

  • -XX:CMSFullGCsBeforeCompaction: 設置進行多少次 CMS 回收後進行碎片整理

G1(Garbage First)

G1(Garbage First) 垃圾收集器是當今垃圾回收技術最前沿的成果之一。早在 JDK7 就已加入 JVM 的收集器大家庭中,成爲 HotSpot 重點發展的垃圾回收技術。

G1 收集器在後臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的 Region。包括:Eden、Survivor、Old 和 Humongous。

其中,Humongous 是特殊的 Old 類型,回收空閒巨型分區,專門放置大型對象。這樣的劃分方式意味着不需要一個連續的內存空間管理對象。G1 將空間分爲多個區域,優先回收垃圾最多的區域。一個對象和它內部所引用的對象可能不在同一個 Region 中,那麼當垃圾回收時,是否需要掃描整個堆內存才能完整地進行一次可達性分析?

當然不是,每個 Region 都有一個 Remembered Set(已記憶集合),用於記錄本區域中所有對象引用的對象所在的區域,從而在進行可達性分析時,只要在 GC Roots 中再加上 Remembered Set 即可防止對所有堆內存的遍歷。

同 CMS 垃圾回收器一樣,G1 也是關注最小時延的垃圾回收器,也同樣適合大尺寸堆內存的垃圾收集,官方也推薦使用 G1 來代替選擇 CMS。G1 最大的特點是引入分區的思路,弱化了分代的概念,合理利用垃圾收集各個週期的資源,解決了其他收集器甚至 CMS 的衆多缺陷。

G1收集器的運作大致分爲以下幾個步驟:

  • 初始標記: 初始標記階段僅僅只是標記一下 GC Roots 能直接關聯到的對象,並且修改 TAMS 的值,讓下一個階段用戶程序併發運行時,能在正確可用的 Region 中創建新對象,這一階段需要停頓線程,但是耗時很短。

  • 併發標記: 併發標記階段是從 GC Root 開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序併發執行。

  • 最終標記: 而最終標記階段則是爲了修正在併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程 Remenbered Set Logs 裏面,最終標記階段需要把Remembered Set Logs 的數據合併到 Remembered Set Logs 裏面,最終標記階段需要把 Remembered Set Logs 的數據合併到 Remembered Set 中,這一階段需要停頓線程,但是可並行執行。

  • 篩選回收: 最後在篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所期望的 GC 停頓時間來制定回收計劃。

在這裏插入圖片描述
G1 能充分利用多 CPU、多核環境下的硬件優勢,使用多 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓的時間,部分其他收集器原本需要停頓 Java 線程執行的GC動作,G1 收集器仍然可以通過併發的方式讓 Java 程序繼續執行。

此外,與其他收集器一樣,分代概念在G1中依然得以保留。雖然 G1 可以不需其他收集器配合就能獨立管理整個 GC 堆,但它能夠採用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次 GC 的舊對象以獲取更好的收集效果。

空間整合: 與 CMS 的 標記-清理 算法不同,G1 從整體看來是基於 標記-整理 算法實現的收集器,從局部(兩個 Region 之間)上看是基於 複製 算法實現,無論如何,這兩種算法都意味着 G1 運作期間不會產生內存空間碎片,收集後能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象時不會因爲無法找到連續內存空間而提前觸發下一次 GC。

可預測的停頓: 這是 G1 相對於 CMS 的另外一大優勢,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過 N 毫秒,這幾乎已經是實時 Java(RTSJ)的垃圾收集器特徵了。

參數控制:-XX:+UseG1GC

 

小結

本文介紹了常見的7種不同分代的收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;而它們所處區域,則表明其是屬於新生代收集器還是老年代收集器:

  • 新生代收集器: Serial、ParNew、Parallel Scavenge

  • 老年代收集器: Serial Old、Parallel Old、CMS

  • 整堆收集器: G1

根據收集的區域(年輕代或年老代)和收集器自身的特性,可以有如下組合:
Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel/Serial Old、Parallel/Parallel Old、G1。

ZGC 來了。ZGC 是 JDK11 中要發佈的最新垃圾收集器。完全沒有分代的概念,官方給出 ZGC 的優點是無碎片,時間可控,超大堆。讀者可以嘗試瞭解和使用一下 ZGC 。

 

垃圾收集器 串行、並行or併發 新生代/老年代 算法 目標 適用場景
Serial 串行 新生代 複製算法 響應速度優先 單CPU環境下的Client模式。單線程、Client模式下默認新生代收集器
Serial Old 串行 老年代 標記-整理 響應速度優先 單CPU環境下的Client模式、CMS的後備預案。Serial的老年代版本、單線程、Client模式下使用
ParNew 並行 新生代 複製算法 響應速度優先 多CPU環境時在Server模式下與CMS配合。Serial的多線程版本、Server模式下默認收集器、默認線程數=CPU數量
Parallel Scavenge 並行 新生代 複製算法 吞吐量優先 在後臺運算而不需要太多交互的任務。多線程、目標關注吞吐量
Parallel Old 並行 老年代 標記-整理 吞吐量優先 在後臺運算而不需要太多交互的任務。Parallel Scavenge的老年代版本、多線程、關注吞吐量
CMS 併發 老年代 標記-清除 響應速度優先 併發低停頓、關注最短停頓時間。集中在互聯網站或B/S系統服務端上的Java應用
G1 併發 新生代+老年代 標記-整理+複製算法 響應速度優先 面向服務端應用,將來替換CMS
  • 吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)
  • 停頓時間短則響應速度好提升用戶體驗;高吞吐量則CPU利用率高,適合後臺運算

 

Reference

發佈了159 篇原創文章 · 獲贊 28 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章