1. 程式人生 > >記憶體溢位分析之垃圾回收知識

記憶體溢位分析之垃圾回收知識

一.Java 記憶體區域劃分

Java記憶體區域劃分
圖片來源Java虛擬機器規範
除上圖執行時資料區域之外,還有一塊記憶體區域被頻繁的使用,那就是直接記憶體,未定義在 《Java虛擬機器規範中》中。

二.垃圾回收

1.可達性分析:

當一個物件到 GC Roots 沒有任何引用鏈時,即從 GC Roots 到這個物件不可達,則說明該物件不可用,可被垃圾回收器回收。

注:如果類實現了 finalize() 方法,則被回收前會先呼叫該方法,且只會呼叫一次。

2.垃圾收集演算法:

標記清除 (Mark-Sweep)

標記出所有需要回收的物件,標記完成後統一回收被標記的物件。

主要缺點:效率不高;產生大量記憶體碎片。記憶體碎片太多可能會導致分配較大物件時,找不到連續記憶體而觸發 GC。

複製 (Copying)

將記憶體劃分為大小相等的兩塊,每次只使用其中一塊。當這塊記憶體用完了,則將存活物件複製到另一塊上,然後將已使用的記憶體空間一次清理掉。適用於多用於新生代。

優點:演算法簡單高效,且無記憶體碎片
缺點:犧牲了空間

標記整理 (Mark-Compact)

標記出需要回收的物件,讓所有存活的物件都向一端移動,然後清理掉端另一邊的記憶體。

分代收集演算法

根據物件存活週期的不同,將記憶體分為幾塊。一般為新生代和老年代(jdk 1.8移除了永久代)。新生代存活物件少,採用複製演算法;老年代物件存活率高,採用標記-清除或標記-整理演算法。

IBM 研究表明,新生代中 98% 物件存活率很低,所以不需要按照 1:1 的比例來劃分記憶體空間,而是使用一塊較大的 Eden

區和兩塊較小的 Survivor 區,每次 YGC 時將 Eden 區和其中一塊 Survivor 區的存活物件複製到另一個 Survivor 區,再一次性清除 Eden 區和使用過的 Survivor 區。

Eden 區和 Survivor 區預設比例大小為 8:1,犧牲 Young 區10%的空間,可通過 SurvivorRatio 引數指定。

3.垃圾收集器

HotSpot 虛擬機器的垃圾收集器(圖片網上找的):
垃圾收集器種類
如果兩個收集器之間存在連線,則說明它們可以搭配使用。

3.1 CMS 收集器

基於標記-清除演算法。只有 CMS initial markCMS Final Remark

階段會 Stop The World

初始標記(CMS initial mark)

標記那些直接被 GC Roots 引用或者被年輕代存活物件所引用的所有物件,會 Stop The World

併發標記(CMS concurrent mark)

遍歷整個 old 區,併發標記存活物件。和應用執行緒並行執行,此時有些物件可能會改變可達狀態。

併發預清理(CMS concurrent preclean)

併發階段,和應用執行緒並行執行。在前面併發執行的階段中,有些物件的引用可能會發生變化,JVM 會將包含該物件的區域 (Card) 標記為 Dirty (即 Card Marking)。

pre-clean 階段,那些能從 Dirty Card 物件到達的物件也會被標記,標記完成之後,Dirty Card 標記會被清除。

此外,還會執行一些必要的清理和為 Final Remark 階段做一些準備工作。

併發預清理(CMS concurrent abortable preclean)

併發執行,這個階段會盡可能的減輕 Final Remark 階段 Stop The World 的壓力。

這個階段的時間依賴於很多因素,會重複做相同的事情,直至滿足一些條件,如:重複的次數、有效的工作量、始終時間等。

重新標記(CMS Final Remark)

完成標記整個 old 區所有的可達物件,會 Stop The World

併發清除(CMS concurrent sweep)

併發移除不可達物件,並回收空間。

併發重置(CMS concurrent reset)

併發重置 CMS 內部資料結構,為下個週期做準備。

3.2 GC 日誌

3.2.1 GC 日誌 JVM 配置

-XX:+PrintGCDetails // 輸出GC的詳細日誌
-XX:+PrintHeapAtGC // 在 GC 前後列印堆記憶體資訊
-XX:+PrintGCDateStamps // 以日期的形式輸出GC的時間戳,如2018-09-29T16:00:43.652+0800,下面例項中未配置該引數
-Xloggc:/tmp/gc/gc.log // 指定 gc 日誌檔案路徑

3.2.2 YGC/Minor GC 日誌

{Heap before GC invocations=8 (full 1):
 par new generation   total 306688K, used 285903K [0x0000000080000000, 0x0000000094cc0000, 0x0000000094cc0000)
  eden space 272640K, 100% used [0x0000000080000000, 0x0000000090a40000, 0x0000000090a40000)
  from space 34048K,  38% used [0x0000000090a40000, 0x0000000091733e10, 0x0000000092b80000)
  to   space 34048K,   0% used [0x0000000092b80000, 0x0000000092b80000, 0x0000000094cc0000)
 concurrent mark-sweep generation total 1756416K, used 15848K [0x0000000094cc0000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 44197K, capacity 45500K, committed 46108K, reserved 1089536K
  class space    used 5199K, capacity 5366K, committed 5472K, reserved 1048576K
23.587: [GC (Allocation Failure) 23.587: [ParNew: 285903K->33786K(306688K), 0.1286737 secs] 301751K->49634K(2063104K), 0.1287772 secs] [Times: user=0.05 sys=0.11, real=0.13 secs] 
Heap after GC invocations=9 (full 1):
 par new generation   total 306688K, used 33786K [0x0000000080000000, 0x0000000094cc0000, 0x0000000094cc0000)
  eden space 272640K,   0% used [0x0000000080000000, 0x0000000080000000, 0x0000000090a40000)
  from space 34048K,  99% used [0x0000000092b80000, 0x0000000094c7e838, 0x0000000094cc0000)
  to   space 34048K,   0% used [0x0000000090a40000, 0x0000000090a40000, 0x0000000092b80000)
 concurrent mark-sweep generation total 1756416K, used 15848K [0x0000000094cc0000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 44197K, capacity 45500K, committed 46108K, reserved 1089536K
  class space    used 5199K, capacity 5366K, committed 5472K, reserved 1048576K
}

展示了 YGC 前後堆記憶體資訊,以及 YGC 日誌。

23.587: [GC (Allocation Failure) 23.587: [ParNew: 285903K->33786K(306688K), 0.1286737 secs] 301751K->49634K(2063104K), 0.1287772 secs] [Times: user=0.05 sys=0.11, real=0.13 secs] 

23.587:GC發生的時間。含義為自 JVM 啟動以來經過的秒數。

GC:表示是 Minor GC 還是 Full GC,此處為 Minor GC

Allocation Failure:GC 原因。

ParNew: GC 發生的區域。該名稱和使用的垃圾收集器密切相關,此處表示使用的是 ParNew 收集器,即 Parallel New GenerationSerial 收集器對應 DefNewParallel Scavenge 收集器對應 PSYoungGen

285903K->33786K(306688K), 0.1286737 secs: GC 前該記憶體區域已使用容量 -> GC 後該記憶體區域已使用容量(該記憶體區域總容量),GC 時間為 0.1286737 秒。

301751K->49634K(2063104K):GC 前 Java 堆已使用容量 -> GC 後 Java 堆已使用容量(Java 堆總容量)

[Times: user=0.05 sys=0.11, real=0.13 secs] :與 Linux time 命令輸出的時間含義一致,分別為 使用者態消耗的 CPU 時間、核心態消耗的 CPU 時間、操作從開始到結束所經過的時鐘時間。CPU 時間和時鐘時間區別在於,時鐘時間包括各種非計算的等待耗時,如執行緒阻塞時間。

3.2.3 CMS GC日誌

7.428: [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(1756416K)] 120764K(2063104K), 0.0254557 secs] [Times: user=0.08 sys=0.00, real=0.03 secs] 
7.453: [CMS-concurrent-mark-start]
7.466: [CMS-concurrent-mark: 0.013/0.013 secs] [Times: user=0.03 sys=0.01, real=0.01 secs] 
7.467: [CMS-concurrent-preclean-start]
7.476: [CMS-concurrent-preclean: 0.010/0.010 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
7.476: [CMS-concurrent-abortable-preclean-start]
11.917: [CMS-concurrent-abortable-preclean: 2.707/4.441 secs] [Times: user=11.40 sys=0.32, real=4.44 secs] 
11.917: [GC (CMS Final Remark) [YG occupancy: 167918 K (306688 K)]11.917: [Rescan (parallel) , 0.0353793 secs]11.953: [weak refs processing, 0.0000242 secs]11.953: [class unloading, 0.0089659 secs]11.962: [scrub symbol table, 0.0050880 secs]11.967: [scrub string table, 0.0005698 secs][1 CMS-remark: 0K(1756416K)] 167918K(2063104K), 0.0521882 secs] [Times: user=0.15 sys=0.00, real=0.05 secs] 
11.970: [CMS-concurrent-sweep-start]
11.970: [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
11.970: [CMS-concurrent-reset-start]
12.000: [CMS-concurrent-reset: 0.030/0.030 secs] [Times: user=0.06 sys=0.03, real=0.03 secs] 

展示了 CMS GC 的各個階段,各階段內容見上面簡介。

7.428: [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(1756416K)] 120764K(2063104K), 0.0254557 secs] [Times: user=0.08 sys=0.00, real=0.03 secs] 

CMS Initial Mark 階段日誌。

  • CMS-initial-mark:表明階段
  • 0K(1756416K)]Old 區佔用和容量
  • 120764K(2063104K):堆佔用和容量
  • 0.0254557 secs] [Times: user=0.08 sys=0.00, real=0.03 secs]:階段執行時間
11.917: [GC (CMS Final Remark) [YG occupancy: 167918 K (306688 K)]11.917: [Rescan (parallel) , 0.0353793 secs]11.953: [weak refs processing, 0.0000242 secs]11.953: [class unloading, 0.0089659 secs]11.962: [scrub symbol table, 0.0050880 secs]11.967: [scrub string table, 0.0005698 secs][1 CMS-remark: 0K(1756416K)] 167918K(2063104K), 0.0521882 secs] [Times: user=0.15 sys=0.00, real=0.05 secs] 

CMS Final Remark 階段日誌:

  • YG occupancy: 167918 K (306688 K): Young 區當前佔用和容量
  • Rescan (parallel) , 0.0353793 secs: 在應用停止的時候,併發標記存活物件
  • weak refs processing, 0.0000242 secs: 子階段,處理弱引用
  • class unloading, 0.0089659 secs: 子階段,解除安裝無用的類
  • scrub symbol table, 0.0050880 secs: 清理 symbol table,包含類的元資料
  • scrub string table, 0.0005698 secs: 清理 string table,包含內部字串
  • CMS-remark: 0K(1756416K)] 167918K(2063104K), 0.0521882 secs: 這個階段執行完後,Old 區的佔用和容量

3.記憶體分配和回收策略

// TODO

三.JDK 命令列工具

常用命令如下

jps

列出正在執行的虛擬機器程序。
jps -v: 列出虛擬機器程序啟動時的 JVM 引數
jps -l: 輸出主類全名,如果執行的是 jar 包,則輸出 jar 包路徑

jps 例項

jps

jstat

監控虛擬機器各種執行狀態資訊。
jstat -gc <pid>: 按實際大小輸出 Java 堆資訊
jstat -gcutil <pid>: 按百分比方式輸出 Java 堆資訊
jstat -class <pid>:輸出類載入、解除安裝、總空間以及載入類所消耗的時間
jstat -gccapacity <pid>: 與 -gc 基本相同,主要輸出各個區域使用到的最大、最小空間
jstat -gccause <pid>: 和 -gcutil 功能一樣,會額外輸出上一次 GC 的原因

jstat -gcnew <pid>: 新生代 GC
jstat -gcnewcapacity <pid>: 和 -gcnew 基本相同,主要關注使用到的最大、最小空間
jstat -gcold <pid>: 老年代 GC
jstat -gcoldcapacity <pid>: 和 -gcold 基本相同,主要關注使用到的最大、最小空間

jstat -compiler <pid>:輸出 JIT 編譯器編譯過的方法、耗時等資訊
jstat -printcompilation <pid>:輸出已經被 JIT 編譯過的方法
注:上述命令後加[interval [s|ms]] [count] 表示查詢間隔和次數。

jstat例項

jstat

jinfo

實時檢視和調整 JVM 各項引數。
格式: jinfo [option] <pid>
檢視未被顯示指定的引數的預設值: jinfo -flag name <pid>,顯示指定通過 jps -v <pid>
檢視引數預設值 JVM 配置: -XX:PrintFlagsFinal
修改可寫的引數: jinfo -flag [+|-] name <pid>jinfo -flag name=value <pid>
檢視 System.getProperties() 內容: jinfo -sysprops <pid>

jinfo 例項

jinfo

jmap

生成堆轉儲快照,即 dump 檔案。
-dump: 生成堆轉儲快照。格式為 -dump:[live]format=b,file=<filepath>,其中 live 表示只 dump 存活物件
-heap: 顯示 Java 堆詳細資訊,如使用哪種回收器、引數配置、分代狀況等
-histo: 顯示堆中物件統計資訊,包括類、例項數量、合計容量
注:jmap 命令可能引起 Stop The World,其中 -histo:live 會觸發 FGC。

jmap -heap 例項

jmap -heap

jmap -dump 例項

jmap -dump

jstack

生成 JVM 當前執行緒快照,稱為 threaddump 檔案。主要目的是定位執行緒出現長時間停頓的原因,如死鎖、死迴圈、請求外部資源時間過長等。
格式: jstack [option] <pid>
-F: 正常輸出的請求不被響應時,強制輸出執行緒堆疊
-l: 除堆疊外,顯示關於鎖的附加資訊
-m: 如果呼叫本地方法,可顯示 C/C++ 的堆疊
注: 建議將 jstack 結果儲存至檔案中,即 jstack [option] <pid> > filepath

jstack 例項

命令: jstack 23282
jstack

四.相關知識

1.CMS GC, Full GC, System.gc()

CMS GC

分為 background, foreground 兩種模式。

background 模式:後臺執行。觸發條件如 old 區記憶體超過一定閾值,會經歷 CMS GC 的所有階段,有暫停,有並行,效率較高。

foreground 模式:前臺執行。觸發條件如執行緒請求分配記憶體時,但是記憶體不夠,這個時候必須等記憶體分配到了,執行緒才繼續往下走,因此全程 Stop The World,但只走其中的幾個階段,效率較低。

Full GC

分為正常 Full GC,並行 Full GC 兩種
正常 Full GC:整個 GC 過程,包括 YGC 和 CMS GC(但呼叫 Full GC介面,兩種 GC 不一定都會執行),其中 CMS GC 為 forebackground 模式
並行 Full GC:效率較高,CMS GC 為 background 模式

System.gc()

System.gc() 是一次 Full GC,會暫停整個程序,因此線上一般會通過 -XX:+DisableExplicitGC 禁用。在 CMS 收集器中可以通過 -XX:+ExplicitGCInvokesConcurrent 指定 並行 Full GC 方式,來做一次效率更高的 GC。

2.HeapByteBuffer 和 DirectByteBuffer

Java NIO 使用 Buffer 作為和 Channel 互動的工具。而 ByteBuffer 主要有兩個子類: HeapByteBuffer, DirectByteBuffer。通過 ByteBuffer 申請方式:

// 申請 HeapByteBuffer
public static ByteBuffer allocate(int capacity) {
	if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}
// 申請 DirectByteBuffer
public static ByteBuffer allocateDirect(int capacity) {
	return new DirectByteBuffer(capacity);
}

DirectByteBuffer

使用 malloc() 在 Java 堆外分配記憶體。

通過 Unsafe 介面 os:malloc 分配記憶體,將記憶體地址和大小存在 DirectByteBuffer 物件中,直接操作記憶體。只有在 DirectByteBuffer 物件被回收了,才能回收這些記憶體。因此,如果這些物件移到了 Old 區,而沒有執行 CMS GCFull GC,則實體記憶體可能會被耗盡。可通過 JVM 引數:-XX:MaxDirectMemorySize 設定最大直接記憶體大小,到達閾值時執行 System.gc(),前提是未被禁用。

HeapByteBuffer

封裝 byte[] 陣列,在 Java 堆記憶體分配。

當使用 HeapByteBuffer 進行網路通訊等 IO 操作時,由於只有“本地”記憶體才能傳遞給作業系統呼叫,此時會將資料複製到一個臨時的 DirectByteBuffer 物件中,JDK 會為每個執行緒快取這個臨時物件,且不限制記憶體大小。因此,如果程式有多個執行緒生成很多大 HeapByteBuffer 物件時,且執行緒一直存活,則會導致程序會佔用大量的本地記憶體,造成記憶體洩露。

程式碼重現

// TODO

解決

  1. JDK 9提供了 JVM 引數: -Djdk.nio.maxCachedBufferSize=262144,來限制這個快取的大小;
  2. 直接使用 DirectByteBuffer,或考慮 Netty 等 NIO 框架。

五.參考資料