JAVA系列-GC

1. OOM

1) 常見OOM異常類型

(1) StackOverFlowError

棧內存溢出,用於深度方法調用(循環遞歸);

(2) OutOfMemoryError:Java heap space

用於變量申請的空間大於jvm的最大值;

(3) OutOfMemoryError: GC overhead limit exceed

GC回收的時間過長時會拋出OutOfMemoryError,過長的定義是,超過98%的時間用來做GC並且回收了不到2%的堆內存,連續多次GC都只回收了不到2%的極端情況下才會拋出。假如不拋出GC overhead limit 錯誤會發生什麼情況?那就是GC清理的這麼點內存很快會再次填滿,迫使GC再次執行,這樣就形成惡性循環,CPU使用率一直是100%,而GC卻沒有任何成果.

JAVA系列-GC

(4) OutOfMemoryError: Direct buffer memory

堆外內存溢出,主要呈現在寫NIO程序經常使用ByteBuffer來讀取或者寫入數據,這是一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆裡面的DirectByteBuffer對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回複製數據。

(5) OutOfMemoryError: unable to create new native thread

你的應用創建了太多線程了,一個應用進程創建了多個線程,超過系統承載極限,你的服務器並不允許你的應用程序創建這麼多線程,linux系統默認允許單個進程可以創建的線程數是1024個,你的應用創建超過這個數量,就會報此錯誤;

  • linux查看當前用戶的線程創建數量(被允許):ulimit -u;
  • 修改當前用戶的被允許的線程創建數量:.vim /etc/security/limits.d/90-nproc.conf

(6) OutOfMemoryError: Metaspace

元空間的本質和永久代類似,都是對JVM規範中方法區的實現,不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此默認情況下,元空間的大小僅受本地內存的限制。

JAVA系列-GC

2. 垃圾回收算法

1) 垃圾回收算法

GC算法是內存回收的方法論,常用的垃圾回收算法:

  • 引用計數法:新生代
  • 複製拷貝算法(Copying):新生代
  • 標計清除(Mark-Sweep):老年代
  • 標計壓縮/整理(Mark-Compact):老年代

2) 垃圾回收器

a. 說明:

針對垃圾回收算法的具體實現,目前為止,還沒有完美的收集器出現,更加沒有萬能的收集器,只是針對具體應用最合適的收集器,進行分代收集

JAVA系列-GC

b. 4種主要的垃圾收集器

  • Serial: 串行垃圾收集器(-XX:+UseSerialGC) - 它為單線程環境設計且只使用一個線程進行垃圾回收,會暫停所有的用戶線程。所以不適合服務器環境;
  • Parallel: 並行垃圾收集器(-XX:+UseParallelGC) - 多個垃圾收集線程並行工作,此時用戶線程是暫停的,適用於科學計算/大數據處理等弱交互場景;
  • CMS: 併發垃圾收集器(-XX:+UseCMSGC) - 用戶線程和垃圾收集線程同時執行(不一定是並行,可能交替執行),不需要停頓用戶線程,互聯網公司多用它,適用對響應時間有要求的場景;
  • G1: G1垃圾回收器(-XX:+UseG1GC) - 將堆內存分割成不同的區域然後併發的對其進行垃圾回收;

JAVA系列-GC

垃圾回收器擴展知識:

  • https://www.cnblogs.com/cxxjohnson/p/8625713.html
  • https://www.cnblogs.com/czwbig/p/11127159.html

c. 7種垃圾回收器

查看默認的垃圾回收器

  • JVM參數:java -XX:+PrintCommandLineFlags -version
<code>-XX:G1ConcRefinementThreads=8 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
java version "13.0.2" 2020-01-14
Java(TM) SE Runtime Environment (build 13.0.2+8)
Java HotSpot(TM) 64-Bit Server VM (build 13.0.2+8, mixed mode, sharing)/<code>

Java的gc回收的類型主要有幾種:

  • UseSerialGC: 串行新生代GC(複製拷貝)
  • UseSerialOldGC: 串行老年代GC(已廢棄)
  • UseParallelGC: 並行新生代GC(複製算法)
  • UseConcMarkSweepGC: 併發老年代GC(標記清除)
  • UseParNewGC : 並行新生區GC
  • UseParallelOldGC :並行老生區GC
  • UseG1GC : G1GC覆蓋新生代和老年代

7種垃圾回收器針對的回收區域:

JAVA系列-GC

GC回收器的腦圖說明

JAVA系列-GC

查看是否用到的某個垃圾回收器

<code>//查看當前java進程
# jps -l
30514 jdk.jcmd/sun.tools.jps.Jps
27507 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
27625 org.jetbrains.jps.cmdline.Launcher

//查看進程的某個jvm屬性
#jinfo -flags 27625
VM Flags:
-XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=134217728 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=734003200 -XX:MaxNewSize=440401920 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5836300 -XX:NonProfiledCodeHeapSize=122910970 -XX:ProfiledCodeHeapSize=122910970 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC
#jinfo -flag UseG1GC 27625
-XX:+UseG1GC

#jinfo -flag UseSerialGC 27625
-XX:-UseSerialGC/<code>

(3) 垃圾收集器參數配置

預設參數說明:

  • DefNew => Default New Generation
  • Tenured => Old
  • ParNew => Parallel New Generation
  • PSYoungGen => Parallel Scavenge
  • ParOldGen => Parallel Old Generation

JVM Server/Client模式: 當前基本64位系統使用的是JVM Server模式

A. 新生代回收器

a) 串行回收器:Serial收集器

一個單線程的收集器,在進行垃圾收集時候,必須暫停其他所有的工作線程直到它收集結束。串行收集器是最古老,最穩定定以及效率高的收集器,只使用一個線程去回收但其在進行垃圾回收過程中可能會產生較長的停頓(Stop-The-World狀態)。雖然在收集垃圾過程中需要暫停所有的其他的工作線程,但是它簡單高效,對於限定單個CPU環境來說,沒有線程交互的開銷可以獲得最高的單線程垃圾收集效率,因此Serial垃圾收集器依賴是JVM運行在Client模式下默認的新生代垃圾收集器。

JAVA系列-GC

JVM參數說明:

  • 參數:-XX:+UseSerialGC
  • 效果: Serial(Young區用) + Serial Old(Old區用)的收集器組合,新生代、老年化都會使用串行回收收集器
  • 算法:新生代使用複製算法,老年代使用標記-整理算法;
<code>-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UserSerialGC/<code>

b) 並行回收器:ParNew

使用多線程進行垃圾回收,在垃圾收集時,會Stop-the-World暫停其他所有的工作線程直到它收集結束。ParNew收集器其實就是Serial收集器新生代的並行多程程版本,最常見的應用場景是配合老年代的CMS GC工作,其餘的行為和Serial收集器完全一樣,ParNew垃圾收集器在垃圾收集過程中同樣也要暫停所有其他的工作線程。它是很多JVM運行在Server模式下新生代的默認垃圾收集器。

JAVA系列-GC

JVM參數說明:

  • 參數:-XX:+UseParNewGC;啟用ParNew收集器隻影響新生代的收集,不影響老年代。
  • 效果:會使用ParNew(Young區用) + Serial Old的收集器組合(java8後,serial Old已不推薦使用);
  • 算法:新生代使用複製算法,老年代採用標記-整理算法

備註:-XX:ParallelGCThreads 限制GC線程數量,默認開啟和CPU數日相同的線程數

<code>-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC/<code>

c) 並行回收器:Parallel/Parallel Scavenge

Parallel Scavenge收集器類似ParNew也是一個新生代垃圾收集器,使用複製算法,也是一個並行的多線程的垃圾收集器,俗稱吞吐量優先收集器。一句話:串行收集器在新生代和老年代的並行化;

此收集器關注的重點:

  • 可控制的吞吐量(Thoughput=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)
    ,也即比如程序運行100分鐘,垃圾收集時間1分鐘,吞吐量就是99%)。高吞吐量意味著高效利用CPU的時間,它多用於後臺運算而不需要太多交互的任務。
  • 自適應調節策略也是ParallelScavenge收集器與ParNew收集器的一個重要區別。(自適應調節策略:虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間(-XX:+MaxGCPauseMillis)或最大的吞吐量)。
JAVA系列-GC

JVM參數說明:

  • 參數:-XX:+UseParallelGC/-XX:+UseParallelOldGC(可以互相激活),啟用Parallel Scanvenge收集器
  • 算法:新生代使用複製算法,老年代使用標記-整理算法

說明:

  • -XX:ParallelGCThreads=數字N :表示啟動多個少GC線程,當cpu>8 N=5/8; 當cpu<8時,N=實際個數
<code>-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC/<code>

B. 老年代回收器

a) 並行收集器:Parallel Old收集器

其是Parallel Scavenge的老年代版本,使用多線程的標記-整理算法,Parallel Old收集器在JDK1.6才開始提供。

  • JDK1.6之前,新生代:ParallelScavenge收集器 + 老年代Serial Old收集器,此組合只能保證新生代的吞吐量優先,無法保證整體的吞吐量。Parallel Old是為了在老年代同樣提供吞吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,
  • JDK1.8後可以優先考慮新生代:Parallel Scavenge+年老代:Parallel Old收集器
    的搭配策略。
JAVA系列-GC

JVM參數說明:

  • -XX:+UseParallelOldGC :啟用Parallel Old收集器,設置此參數後,新生代Parallel Scavenge + 老年代Parallel Old同時啟用;
<code>-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC/<code>

b) 併發標記清除回收器:CMS(Concurrent Mark Sweep)

是一種以獲取最短回收停頓時間為目標的收集器,適合應用在互聯網站或者B/S系統的服務器上,這類應用尤其重視服務器的響應速度,希望系統停頓時間最短,CMS非常適合堆內存大、CPU核數多的服務器端應用,也是G1出現之前大型應用的首選收集器

Concurrent Mark Sweep併發標記清除,併發收集低停頓,併發指的是與用戶線程一起執行

JAVA系列-GC

cms回收過程:

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

  • 初始標記(CMS initial mark):只是標記一下GC Roots能直接關聯的對象,速度很愉,仍然需要暫停所有工作線程;
  • 併發標記(CMS concurrent mark):進行GC Roots跟蹤的過程,和用戶線程一起工作,不需要暫停工作線程。主要標記過程,標記全部對象;
  • 重新標記(CMS remark):為了修正在併發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,仍然需要暫停所有的工作線程(由於併發標記時,用戶線程依然運行,因此在正式清理前,再做修正);
  • 併發清除(CMS concurrent sweep):清除GC Roots不可達對象,和用戶線程一起工作,不需要暫停工作線程。基於標記的結果,直接清理對象;
JAVA系列-GC

由於耗時最長的併發標記和併發清除過程中,垃圾收集線程可以和用戶線程一起併發工作,所以總體上來看CMS收集器的內存回收和用戶線程是一起併發地執行。

優缺點:

  1. 優點: 併發收集,低停頓
  2. 缺點:併發執行,對CPU資源壓力大:由於併發進行,CMS在收集與應用線程會同時增加對堆內存的佔用,也就是說CMS必須要在老年代堆內存用盡之前完成垃圾回收,否則CMS回收失敗時,將觸發擔保機制,串行老年代收集器將會以STW的方式進行一次GC,從而造成較大停頓時間。採用的標記清除算法會導致大量碎片:標記清除算法無法整理空間碎片,老年代空間會隨著應用時長被耗盡,最後將不得不通過擔保機制對堆內存進行壓縮。CMS也提供了參數-XX:CMSFullGCsBeForeCompaction(默認0,即每次都進行內存整理)來指定多少次CMS收集後,進行一次壓縮的Full GC。

JVM參數說明:

  • 參數:-XX:+UseConcMarkSweepGC 開啟該參數後會自動將-XX:+UseParNewGC打開
  • 效果:開啟上述參數後會使用ParNew(Young區用) + CMS(Old區用) + Serial Old的收集器組合, Serial Old將作為CMS出錯後的備收集器
<code>-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC/<code>

c) 串行標記清除回收器:Serial Old

Serial Old是Serial垃圾收集器的老年代版本,它同樣是個單線程收集器,使用標記-整理算法,這個收集器也主要是運行在Client的JVM上的默認老年代垃圾收集器

它在Server模式下,主要有兩上用途:

  • JDK1.5前版本中與新生代的Parallel Scavenge收集器搭配使用。(Parallel Scavenge + Serial Old)
  • 作為老年代中使用CMS收集器的後備垃圾收集方案;
<code>-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC/<code>

C. G1回收器

以前收集器的特點

  1. 年輕代和老年代是各自獨立且連續的內存塊;
  2. 年輕代收集使用單eden + s0 + s1進行復制算法;
  3. 老年代收集必須掃描整個老年代區域;
  4. 都是以儘可能少而快速地執行GC為設計原則;

G1的特點(JDK9中將G1變成默認的垃圾收集器以替代CMS)

G1(Garbage First)是一種服務端的垃圾收集器,應用在多處理器和大容量內存環境中,極少的減少垃圾收集的停頓時間,在實現高吞吐量的同時,儘可能的滿足垃圾收集暫停時間的要求。主要改變的是Eden, Survivor和Tenured等內存區域不再是連續的了,而是變成了一個個大小一樣的region,每個region從1M到32M不等。一個region有可能屬於Eden,Survivor或者Tenured內存區域。

具有以下特點:

  • 像CMS收集器一樣,能與應用程序線程併發執行;
  • 整理空閒空間更快;
  • 需要更多的時間來預測GC停頓時間;
  • 不希望犧牲大量的吞吐性能;
  • 不需要更大的Java Heap;

G1收集器的設計目標是取代CMS收集器,它同CMS相比,有如下出色表現:

  • G1是一個有整理內存過程的垃圾收集器,不會產生很多的能存碎片;
  • G1的Stop The World(STV)更可控,G1在停頓時間上添加了預測機制,用戶可以指定期望停頓時間;

G1的整體特性:

  • G1能充分利用多CPU、多核環境硬件優勢,儘量縮短STW;
  • G1整體上採用標記-整理算法,局部是通過複製算法,不會產生內存碎片;
  • 宏觀上看G1之中不再區分年輕代和老年代。把內存劃分成多個獨立的子區域(Region),可以近似理解為一個圍棋的棋盤;
  • G1收集器裡面講整修模的內存區都混合在一起了,但期本身依然在小範圍內要進行年輕代和老年代的共分,保留了年輕代和老年代,但它們不再是物理隔離的,而是一部分Region的集合且不需要Region是連續的,也就是說依然會採用不同的GC方式來處理不同的區域;
  • G1雖然也是分代收集器,但整個內存分區不存在物理上的年輕代與老年代的區別,也不需要完全獨立的survivor(to space)堆做複製準備,G1只有邏輯上的分代概念,或者說每個分區都可能隨G1的運行在不同代之間前後切換;

G1底層原理

Region區域化垃圾收集器:最大的好處是化整為零,避免全內存掃描,只需要按照區域來進行掃描即可。區域化內存劃片Region,整體編為了一些列不連續的內存區域,避免了全內存區的GC操作。核心思想是將整個堆內存區域分成大小相同的子區域(Region),在JVM啟動時會自動設置這些子區域的大小,在堆的使用上,G1並不要求對象的存儲一定是物理上連續的只要邏輯上連續即可,每個分區也不會固定地為某個代服務,可以按需在年輕代和老年代之間切換。啟動時可以通過參數-XX:G1HeapRegionSize=n可指定分區大小(1MB-32MB且必須是2的冪),默認將整堆劃分為2048個分區。(大小範圍在1MB-32MB,最多能設置2048個區域,即能夠支持的最大內存為:32M * 2048 = 64GB)

JAVA系列-GC

G1算法將堆劃分為若干個區域(Region),它仍然屬於分代收集器:

  • 這些Region的一部分包含新生代,新生代的垃圾收集依然採用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間;
  • 這些Region的一部分包含老年代,G1收集器通過將對象從一個區域複製到另外一個區域,完成了清理工作。這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有CMS內存碎片問題的存在了;
  • 在G1中還有一種特殊的區域叫Humongous(巨大的)區域,如果一個對象佔用的空間超過了分區容量的50%以上,G1收集器就認為這是一個巨型對象。這些巨型對象默認直接被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1劃分了一個Humongous區,它用於專門存放巨型對旬。如果一個H構裝不下一個巨型對象,那麼G1會尋找連續的H區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC.

G1的回收步驟

針對Eden區進行收集,Eden區耗盡後會被觸發,主要是小區域收集 + 形成連續的內存塊,避免內存碎片

  • Eden區的數據移動到Survivor區,假如出現Survivor空間不夠,Eden區數據部分會晉升到Old區;
  • Survivor區的數據移動到新的Survivor區,部分數據會晉升到Old區;
  • 最後Eden區收拾乾淨了,GC結束,用戶的應用程序繼續執行;
JAVA系列-GC


JAVA系列-GC

G1四步過程

  • 初始標記:只標記GC Roots能直接關聯到的對象;
  • 併發標記:進行GC Roots Tracing的過程;
  • 最終標記:修正併發標記期間,因程序運行導致標記發生變化的那一部分對象;
  • 篩選回收:根據時間來進行價值最大化的回收;
JAVA系列-GC


G1參數說明

  • -XX:+UseG1GC:開啟G1回收
  • -XX:G1HeapRegionSize=n: 設置的G1中Region的大小,值是2的冪,範圍是1MB~32MB。目標是根據最小的Java堆大小劃分出約2048個區域;
  • -XX:MaxGCPauseMillis=n:最大GC停頓時間,這是個軟目標,JVM將盡可能(但不保證)停頓小於這個時間;
  • -XX:InitiatingHeapOccupancyPercent=n:堆佔用了多少的時候就觸發GC, 默認為45%;
  • -XX:ConcGCThreads=n:併發GC使用的線程數;
  • -XX:G1ReservePercent=n:設置作為空閒空間的預留內存百分比,以降低目標空間溢出的風險,默認值10%;

G1與CMS的對比優勢

  • G1不會產生內存碎片;
  • 可以精確的控制GC停頓。該收集器是把整個堆(新生代、老生代)劃分成多個固定大小的區域,每次根據允許停頓的時間去收集垃圾最多的區域;

(4) 如何選擇垃圾收集器

A. 組合的選擇:

  1. 單CPU或小內存,單機程序:-XX:+UseSerialGC
  2. 多CPU,需要最大吞吐量,如後臺計算型應用:-XX:+UseParallelGC/+UseParallelOldGC
  3. 多CPU,追求低停頓時間,需快速響應如互聯網應用:-XX:UseConcMarkSweepGC 和 -XX:+UseParNewGC

B. 整理表格

JAVA系列-GC


分享到:


相關文章: