1. 程式人生 > 程式設計 >5種JVM垃圾收集器特點和8種JVM記憶體溢位原因

5種JVM垃圾收集器特點和8種JVM記憶體溢位原因

一、5種JVM垃圾收集器特點

1、常見垃圾收集器

現在常見的垃圾收集器有如下幾種: 新生代收集器:
  • Serial
  • ParNew
  • Parallel Scavenge
老年代收集器:
  • Serial Old
  • CMS
  • Parallel Old
堆記憶體垃圾收集器:G1 每種垃圾收集器之間有連線,表示他們可以搭配使用。

2、新生代垃圾收集器

(1)Serial 收集器

Serial 是一款用於新生代的單執行緒收集器,採用複製演演算法進行垃圾收集。Serial 進行垃圾收集時,不僅只用一條執行緒執行垃圾收集工作,它在收集的同時,所有的使用者執行緒必須暫停(Stop The World)。 就比如媽媽在家打掃衛生的時候,肯定不會邊打掃邊讓兒子往地上亂扔紙屑,否則一邊製造垃圾,一遍清理垃圾,這活啥時候也幹不完。 如下是 Serial 收集器和 Serial Old 收集器結合進行垃圾收集的示意圖,當使用者執行緒都執行到安全點時,所有執行緒暫停執行,Serial 收集器以單執行緒,採用複製演演算法進行垃圾收集工作,收集完之後,使用者執行緒繼續開始執行。

適用場景:Client 模式(桌面應用);單核伺服器。 可以用 -XX:+UserSerialGC 來選擇 Serial 作為新生代收集器。

(2)ParNew 收集器

ParNew 就是一個 Serial 的多執行緒版本,其它與Serial並無區別。ParNew 在單核 CPU 環境並不會比 Serial 收集器達到更好的效果,它預設開啟的收集執行緒數和 CPU 數量一致,可以通過 -XX:ParallelGCThreads 來設定垃圾收集的執行緒數。 如下是 ParNew 收集器和 Serial Old 收集器結合進行垃圾收集的示意圖,當使用者執行緒都執行到安全點時,所有執行緒暫停執行,ParNew 收集器以多執行緒,採用複製演演算法進行垃圾收集工作,收集完之後,使用者執行緒繼續開始執行。

適用場景:多核伺服器;與 CMS 收集器搭配使用。當使用 -XX:+UserConcMarkSweepGC 來選擇 CMS 作為老年代收集器時,新生代收集器預設就是 ParNew,也可以用 -XX:+UseParNewGC 來指定使用 ParNew 作為新生代收集器。

(3)Parallel Scavenge 收集器

Parallel Scavenge 也是一款用於新生代的多執行緒收集器,與 ParNew 的不同之處是ParNew 的目標是儘可能縮短垃圾收集時使用者執行緒的停頓時間,Parallel Scavenge 的目標是達到一個可控制的吞吐量。 吞吐量就是 CPU 執行使用者執行緒的的時間與 CPU 執行總時間的比值【吞吐量 = 執行使用者代程式碼時間/(執行使用者程式碼時間+垃圾收集時間)】,比如虛擬機器器一共運行了 100 分鐘,其中垃圾收集花費了 1 分鐘,那吞吐量就是 99% 。比如下面兩個場景,垃圾收集器每 100 秒收集一次,每次停頓 10 秒,和垃圾收集器每 50 秒收集一次,每次停頓時間 7 秒,雖然後者每次停頓時間變短了,但是總體吞吐量變低了,CPU 總體利用率變低了。
可以通過 -XX:MaxGCPauseMillis 來設定收集器儘可能在多長時間內完成記憶體回收,可以通過 -XX:GCTimeRatio 來精確控制吞吐量。 如下是 Parallel 收集器和 Parallel Old 收集器結合進行垃圾收集的示意圖,在新生代,當使用者執行緒都執行到安全點時,所有執行緒暫停執行,ParNew 收集器以多執行緒,採用複製演演算法進行垃圾收集工作,收集完之後,使用者執行緒繼續開始執行;在老年代,當使用者執行緒都執行到安全點時,所有執行緒暫停執行,Parallel Old 收集器以多執行緒,採用標記整理演演算法進行垃圾收集工作。 適用場景:注重吞吐量,高效利用 CPU,需要高效運算且不需要太多互動。 可以使用 -XX:+UseParallelGC 來選擇 Parallel Scavenge 作為新生代收集器,jdk7、jdk8 預設使用 Parallel Scavenge 作為新生代收集器。

3、老年代垃圾收集器

(1)Serial Old 收集器

Serial Old 收集器是 Serial 的老年代版本,同樣是一個單執行緒收集器,採用標記-整理演演算法。 如下圖是 Serial 收集器和 Serial Old 收集器結合進行垃圾收集的示意圖: 適用場景:Client 模式(桌面應用);單核伺服器;與 Parallel Scavenge 收集器搭配;作為 CMS 收集器的後備預案。

(2)CMS(Concurrent Mark Sweep) 收集器

CMS 收集器是一種以最短回收停頓時間為目標的收集器,以 “ 最短使用者執行緒停頓時間 ” 著稱。整個垃圾收集過程分為 4 個步驟: ① 初始標記:標記一下 GC Roots 能直接關聯到的物件,速度較快。 ② 併發標記:進行 GC Roots Tracing,標記出全部的垃圾物件,耗時較長。 ③ 重新標記:修正併發標記階段引使用者程式繼續執行而導致變化的物件的標記記錄,耗時較短。 ④ 併發清除:用標記-清除演演算法清除垃圾物件,耗時較長。 整個過程耗時最長的併發標記和併發清除都是和使用者執行緒一起工作,所以從總體上來說,CMS 收集器垃圾收集可以看做是和使用者執行緒併發執行的。 CMS 收集器也存在一些缺點: 對 CPU 資源敏感:預設分配的垃圾收集執行緒數為(CPU 數+3)/4,隨著 CPU 數量下降,佔用 CPU 資源越多,吞吐量越小 無法處理浮動垃圾:在併發清理階段,由於使用者執行緒還在執行,還會不斷產生新的垃圾,CMS 收集器無法在當次收集中清除這部分垃圾。同時由於在垃圾收集階段使用者執行緒也在併發執行,CMS 收集器不能像其他收集器那樣等老年代被填滿時再進行收集,需要預留一部分空間提供使用者執行緒執行使用。當 CMS 執行時,預留的記憶體空間無法滿足使用者執行緒的需要,就會出現 “ Concurrent Mode Failure ”的錯誤,這時將會啟動後備預案,臨時用 Serial Old 來重新進行老年代的垃圾收集。 因為 CMS 是基於標記-清除演演算法,所以垃圾回收後會產生空間碎片,可以通過 -XX:UserCMSCompactAtFullCollection 開啟碎片整理(預設開啟),在 CMS 進行 Full GC 之前,會進行記憶體碎片的整理。還可以用 -XX:CMSFullGCsBeforeCompaction 設定執行多少次不壓縮(不進行碎片整理)的 Full GC 之後,跟著來一次帶壓縮(碎片整理)的 Full GC。 適用場景:重視伺服器響應速度,要求系統停頓時間最短。可以使用 -XX:+UserConMarkSweepGC 來選擇 CMS 作為老年代收集器。

(3)Parallel Old 收集器

Parallel Old 收集器是 Parallel Scavenge 的老年代版本,是一個多執行緒收集器,採用標記-整理演演算法。可以與 Parallel Scavenge 收集器搭配,可以充分利用多核 CPU 的計算能力。 適用場景:與Parallel Scavenge 收集器搭配使用;注重吞吐量。jdk7、jdk8 預設使用該收集器作為老年代收集器,使用 -XX:+UseParallelOldGC 來指定使用 Paralle Old 收集器。

4、新生代和老年代垃圾收集器

G1 收集器 G1 收集器是 jdk1.7 才正式引用的商用收集器,現在已經成為 jdk9 預設的收集器。前面幾款收集器收集的範圍都是新生代或者老年代,G1 進行垃圾收集的範圍是整個堆記憶體,它採用 “ 化整為零 ” 的思路,把整個堆記憶體劃分為多個大小相等的獨立區域(Region),在 G1 收集器中還保留著新生代和老年代的概念,它們分別都是一部分 Region,如下圖: 每一個方塊就是一個區域,每個區域可能是 Eden、Survivor、老年代,每種區域的數量也不一定。JVM 啟動時會自動設定每個區域的大小(1M ~ 32M,必須是 2 的次冪),最多可以設定 2048 個區域(即支援的最大堆記憶體為 32M*2048 = 64G),假如設定 -Xmx8g -Xms8g,則每個區域大小為 8g/2048=4M。 為了在 GC Roots Tracing 的時候避免掃描全堆,在每個 Region 中,都有一個 Remembered Set 來實時記錄該區域內的引用型別資料與其他區域資料的引用關係(在前面的幾款分代收集中,新生代、老年代中也有一個 Remembered Set 來實時記錄與其他區域的引用關係),在標記時直接參考這些引用關係就可以知道這些物件是否應該被清除,而不用掃描全堆的資料。 G1 收集器可以 “ 建立可預測的停頓時間模型 ”,它維護了一個列表用於記錄每個 Region 回收的價值大小(回收後獲得的空間大小以及回收所需時間的經驗值),這樣可以保證 G1 收集器在有限的時間內可以獲得最大的回收效率。 如下圖所示,G1 收集器收集器收集過程有初始標記、併發標記、最終標記、篩選回收,和 CMS 收集器前幾步的收集過程很相似: ① 初始標記:標記出 GC Roots 直接關聯的物件,這個階段速度較快,需要停止使用者執行緒,單執行緒執行。 ② 併發標記:從 GC Root 開始對堆中的物件進行可達新分析,找出存活物件,這個階段耗時較長,但可以和使用者執行緒併發執行。 ③ 最終標記:修正在併發標記階段引使用者程式執行而產生變動的標記記錄。 ④ 篩選回收:篩選回收階段會對各個 Region 的回收價值和成本進行排序,根據使用者所期望的 GC 停頓時間來指定回收計劃(用最少的時間來回收包含垃圾最多的區域,這就是 Garbage First 的由來——第一時間清理垃圾最多的區塊),這裡為了提高回收效率,並沒有採用和使用者執行緒併發執行的方式,而是停頓使用者執行緒。 適用場景:要求儘可能可控 GC 停頓時間;記憶體佔用較大的應用。可以用 -XX:+UseG1GC 使用 G1 收集器,jdk9 預設使用 G1 收集器。

5、JVM垃圾收集器總結

本文主要介紹了JVM中的垃圾回收器,主要包括序列回收器、並行回收器以及CMS回收器、G1回收器。他們各自都有優缺點,通常來說你需要根據你的業務,進行基於垃圾回收器的效能測試,然後再做選擇。下面給出配置回收器時,經常使用的引數:
-XX:+UseSerialGC:在新生代和老年代使用序列收集器
-XX:+UseParNewGC:在新生代使用並行收集器
-XX:+UseParallelGC :新生代使用並行回收收集器,更加關注吞吐量
-XX:+UseParallelOldGC:老年代使用並行回收收集器
-XX:ParallelGCThreads:設定用於垃圾回收的執行緒數
-XX:+UseConcMarkSweepGC:新生代使用並行收集器,老年代使用CMS+序列收集器
-XX:ParallelCMSThreads:設定CMS的執行緒數量
-XX:+UseG1GC:啟用G1垃圾回收器


二、8種JVM記憶體溢位原因

1、Java 堆空間

發生頻率:5顆星 造成原因
  • 無法在 Java 堆中分配物件
  • 吞吐量增加
  • 應用程式無意中儲存了物件引用,物件無法被 GC 回收
  • 應用程式過度使用 finalizer。finalizer 物件不能被 GC 立刻回收。finalizer 由結束佇列服務的守護執行緒呼叫,有時 finalizer 執行緒的處理能力無法跟上結束佇列的增長
解決方案
  • 使用 -Xmx 增加堆大小
  • 修復應用程式中的記憶體洩漏

2、GC 開銷超過限制

發生頻率:5顆星 造成原因
  • Java 程式98%的時間在進行垃圾回收,恢復了不到2%的堆空間,最後連續5個(編譯時常量)垃圾回收一直如此。
解決方案
  • 使用 -Xmx 增加堆大小
  • 使用 -XX:-UseGCOverheadLimit 取消 GC 開銷限制
  • 修復應用程式中的記憶體洩漏

3、請求的陣列大小超過虛擬機器器限制

發生頻率:2顆星 造成原因
  • 應用程式試圖分配一個超過堆大小的陣列
解決方案
  • 使用 -Xmx 增加堆大小
  • 修復應用程式中分配巨大陣列的 bug

4、Perm gen 空間

發生頻率:3顆星 造成原因 Perm gen 空間包含:
  • 類的名字、欄位、方法
  • 與類相關的物件陣列和型別陣列
  • JIT 編譯器優化
當 Perm gen 空間用盡時,將丟擲異常。 解決方案
  • 使用 -XX: MaxPermSize 增加 Permgen 大小
  • 不重啟應用部署應用程式可能會導致此問題。重啟 JVM 解決

5、Metaspace

發生頻率:3顆星 造成原因
  • 從 Java 8 開始 Perm gen 改成了 Metaspace,在本機記憶體中分配 class 元資料(稱為 metaspace)。如果 metaspace 耗盡,則丟擲異常
解決方案
  • 通過命令列設定 -XX: MaxMetaSpaceSize 增加 metaspace 大小
  • 取消 -XX: maxmetsspacedize
  • 減小 Java 堆大小,為 MetaSpace 提供更多的可用空間
  • 為伺服器分配更多的記憶體
  • 可能是應用程式 bug,修復 bug

6、無法新建本機執行緒

發生頻率:5顆星 造成原因
  • 記憶體不足,無法建立新執行緒。由於執行緒在本機記憶體中建立,報告這個錯誤表明本機記憶體空間不足
解決方案
  • 為機器分配更多的記憶體
  • 減少 Java 堆空間
  • 修復應用程式中的執行緒洩漏。
  • 增加作業系統級別的限制
  • ulimit -a
  • 使用者程式數增大 (-u) 1800
  • 使用 -Xss 減小執行緒堆疊大小

7、殺死程式或子程式

發生頻率:1顆星 造成原因
  • 核心任務:記憶體不足結束器,在可用記憶體極低的情況下會殺死程式
解決方案
  • 將程式遷移到不同的機器上
  • 給機器增加更多記憶體
  • 與其他 OOM 錯誤不同,這是由作業系統而非 JVM 觸發的。

8、發生 stack_trace_with_native_method

發生頻率:1顆星 造成原因
  • 本機方法(native method)分配失敗
  • 列印的堆疊跟蹤資訊,最頂層的幀是本機方法
解決方案
  • 使用作業系統本地工具進行診斷

三、最後

歡迎大家一起交流,喜歡文章記得點個贊喲,感謝支援!