JVM內存分配和垃圾回收

內存分佈

image.png

對象通常在堆上分配,但JVM支持一種在棧上分配內存的機制。
經過-XX:+DoEscapeAnalysis開啓逃逸分析(默認開啓),JVM會針對不會逃逸的對象分配在棧上。好處是,棧能夠自動彈出,不須要垃圾回收參與處理這些對象。html

此外TLAB(Thread Local Allocation Buffer)是一個線程獨佔的堆空間。通常的堆空間是共享的,在內存分配時,多個線程須要同步,但TLAB區域因爲線程獨佔,因此沒必要在分配內存時進行同步。TLAB自己佔用eden區域。java

關於逃逸分析TLAB參見jvm 優化篇-(4)-棧上分配與逃逸分析 -XX:+DoEscapeAnalysis -XX:+UseTLAB -XX:TLABRefillWasteFraction算法

分代算法

現代垃圾回收的基本算法是標記清除(Mark-Sweep),但依然要面臨內存碎片的問題。JVM採用分代機制解決內存碎片問題。多線程

新生代採用複製算法。新生代的特色是,大部分對象是能夠回收的。新生代區進一步分爲eden區、from區、to區。from區和to區是兩塊大小相同內存區域,有時也叫S0/S1,做用是交換存活對象。新生代內存分配發生在eden區。當新生代須要垃圾回收時,假設此時S0中是上一次GC留下來的存活對象,那麼eden中的存活對象和S0中的對象都將複製到S1(並對齊),而後eden和S0能夠直接清空;下一次垃圾回收時,eden和S1的存活對象複製到S0(並對齊)。因此說S0和S1相互交換存活對象。若是S0和S1沒法容納對象,那麼部分對象將進入老年代區。因爲新生代中大部分對象是能夠回收的,因此採用這種複製算法壓縮內存最爲高效。併發

老年代採用標記壓縮算法。由於老年代活動對象多,垃圾對象少。jvm

分區算法

G1採用分區算法。分區的思想是將推內存劃分爲多個區,若是每次只收集若干區域,而不是整個堆,能夠有效的控制停頓時間。函數

垃圾回收器的發展

垃圾回收器經歷了串行、並行(多線程)、併發(不阻塞應用)的發展。參考[深刻JVM讀書筆記(四)——Java的垃圾收集器]性能

  • Serial/Serial Old: 串行收集器,收集過程會阻塞應用程序線程,並用單線程完成收集
  • ParNew: 適用於新生代的並行收集器,收集過程會阻塞應用程序線程,並用多個線程完成對新生代區的收集
  • Parallel Scavenge: 與ParNew相似,但能夠調節停頓時間和吞吐等參數
  • Parallel Old: 針對老年代的並行收集
  • CMS: 針對老年代,可針對某些收集階段支持併發收集。意味着某些狀況下能夠不阻塞應用程序運行
  • G1: 1.7開始引入,同時具備並行、併發能力,同時支持新生代和老年代,並採用分區的思想控制停頓時間。當Java堆很是大的時候,G1的優點更加明顯

早期的垃圾收集器能夠組合使用,以下圖優化

java工程師成神之路:Java工程師成神之路(一)之jvm基礎篇

G1

G1收集器的整體效果是好於CMS的,有更好的自我調節能力而G1從JDK9開始纔是默認垃圾回收器。因此JDK8的狀況下,最好主動設置G1垃圾回收器:spa

-XX:+UseG1GC

G1收集器用Region來劃份內存,雖然邏輯上依然保留新生代和老年代,可是新生代和老年代是由若干Region組成的,而且並不必定要求連續。每一個分區Region也不會肯定地爲某個代服務,能夠按需在新生代和老年代之間切換。

18.657: [GC pause (G1 Evacuation Pause) (young) (initial-mark) 26M->24M(32M), 0.0025448 secs]
18.659: [GC concurrent-root-region-scan-start]
18.660: [GC concurrent-root-region-scan-end, 0.0008815 secs]
18.660: [GC concurrent-mark-start]
18.696: [GC concurrent-mark-end, 0.0357099 secs]
18.696: [GC remark, 0.0037490 secs]
18.703: [GC cleanup 24M->24M(32M), 0.0004163 secs]
18.892: [GC pause (G1 Evacuation Pause) (young) 26M->25M(32M), 0.0027587 secs]
19.014: [GC pause (G1 Evacuation Pause) (mixed) 26M->24M(32M), 0.0042025 secs]

上面是一段G1的gc日誌

  • initial-mark: 初始標記,伴隨一個新生代GC,有暫停
  • concurrent-root-region-scan-start/end: 根區域掃描,併發的無暫停
  • concurrent-mark-start/end: 併發標記,併發的無暫停
  • remark: 從新標記,有暫停
  • cleanup: 獨佔清理,有暫停
  • young: 新生代GC,有暫停
  • mixed: 同時有新生代和老年代GC,無暫停。在併發標記中得知哪些Region垃圾比例比較高,會在這個階段對這些Region進行清理(Gargage First的由來)

併發標記可能被young gc和full gc打斷,例以下面的日誌展現了被full gc中斷的concurrent-mark

34.036: [GC concurrent-mark-start]
34.037: [Full GC (Allocation Failure)  31M->31M(32M), 0.0912206 secs]
34.128: [Full GC (Allocation Failure)  31M->31M(32M), 0.0905478 secs]
34.219: [GC concurrent-mark-abort]
34.219: [GC pause (G1 Evacuation Pause) (young) 31M->31M(32M), 0.0084531 secs]
34.228: [GC pause (G1 Evacuation Pause) (young) (initial-mark) 31M->31M(32M), 0.0067091 secs]

總結

image.png

G1的參數

選項 說明
-XX:MaxGCPauseMillis 設置最大GC停頓時間(GC pause time)指標(target). 這是一個軟性指標(soft goal), JVM 會盡可能去達成這個目標.
-XX:InitiatingHeapOccupancyPercent 啓動併發GC週期時的堆內存佔用百分比. G1之類的垃圾收集器用它來觸發併發GC週期,基於整個堆的使用率,而不僅是某一代內存的使用比. 值爲 0 則表示"一直執行GC循環". 默認值爲 45
-XX:ParallelGCThreads 設置垃圾收集器在並行階段使用的線程數,默認值隨JVM運行的平臺不一樣而不一樣

常見OOM

  • StackOverFlowError: 棧內存溢出,用於深度方法調用(循環遞歸)
  • OutOfMemoryError: Java heap space。用於變量申請的空間大於jvm的最大值
  • OutOfMemoryError: GC overhead limit exceed。GC回收的過長時會拋出OutOfMemoryError,過長的定義是,超過98%的時間用來作GC而且回收了不到2%的堆內存,連續屢次GC都只回收了不到2%的極端狀況下才會拋出。假如不拋出GC overhead limit 錯誤會發生什麼狀況?那就是GC清理的這麼點內存很快會再次填滿,迫使GC再次執行,這樣就造成惡性循環,CPU使用率一直是100%,而GC卻沒有任何成果
  • OutOfMemoryError: Direct buffer memory。堆外內存溢出,主要呈如今寫NIO程序常用ByteBuffer來讀取或者寫入數據,這是一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可使用Native函數庫直接分配堆外內存,而後經過一個存儲在Java堆裏面的DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java堆和Native堆中來回複製數據
  • OutOfMemoryError: unable to create new native thread。應用建立了太多線程
  • OutOfMemoryError: Metaspace。元空間的本質和永久代相似,都是對JVM規範中方法區的實現,不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以默認狀況下,元空間的大小僅受本地內存的限制。

jstat

jstat -gc <pid>
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
1088.0 1088.0  0.0   117.2   8704.0   8641.6   21888.0    21888.0   24832.0 24320.1 2560.0 2340.1     26    0.134  13      0.810    0.944
  • S0C:第一個倖存區的大小
  • S1C:第二個倖存區的大小
  • S0U:第一個倖存區的使用大小
  • S1U:第二個倖存區的使用大小
  • EC:eden區的大小
  • EU:eden區的使用大小
  • OC:老年代大小
  • OU:老年代使用大小
  • MC:方法區大小
  • MU:方法區使用大小
  • CCSC:壓縮類空間大小
  • CCSU:壓縮類空間使用大小
  • YGC: 年輕代垃圾回收次數
  • YGCT:年輕代垃圾回收消耗時間
  • FGC: 老年代垃圾回收次數
  • FGCT:老年代垃圾回收消耗時間
  • GCT:垃圾回收消耗總時間

jinfo

查看進程的jvm flag。例如,驗證DoEscapeAnalysis默認是開啓的

$ jinfo -flag DoEscapeAnalysis 6953
-XX:+DoEscapeAnalysis

也能夠動態修改部分參數

$ jinfo -flag +PrintGCDetails 6953
相關文章
相關標籤/搜索