記憶體溢位分析之垃圾回收知識
一.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 mark
和 CMS 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 Generation
。Serial
收集器對應 DefNew
,Parallel 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 例項
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例項
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 例項
jmap
生成堆轉儲快照,即 dump 檔案。
-dump
: 生成堆轉儲快照。格式為 -dump:[live]format=b,file=<filepath>
,其中 live 表示只 dump 存活物件
-heap
: 顯示 Java 堆詳細資訊,如使用哪種回收器、引數配置、分代狀況等
-histo
: 顯示堆中物件統計資訊,包括類、例項數量、合計容量
注:jmap 命令可能引起 Stop The World
,其中 -histo:live
會觸發 FGC。
jmap -heap 例項
jmap -dump 例項
jstack
生成 JVM 當前執行緒快照,稱為 threaddump 檔案。主要目的是定位執行緒出現長時間停頓的原因,如死鎖、死迴圈、請求外部資源時間過長等。
格式: jstack [option] <pid>
-F
: 正常輸出的請求不被響應時,強制輸出執行緒堆疊
-l
: 除堆疊外,顯示關於鎖的附加資訊
-m
: 如果呼叫本地方法,可顯示 C/C++ 的堆疊
注: 建議將 jstack 結果儲存至檔案中,即 jstack [option] <pid> > filepath
jstack 例項
命令: jstack 23282
四.相關知識
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 GC
或 Full GC
,則實體記憶體可能會被耗盡。可通過 JVM 引數:-XX:MaxDirectMemorySize
設定最大直接記憶體大小,到達閾值時執行 System.gc()
,前提是未被禁用。
HeapByteBuffer
封裝 byte[]
陣列,在 Java 堆記憶體分配。
當使用 HeapByteBuffer
進行網路通訊等 IO 操作時,由於只有“本地”記憶體才能傳遞給作業系統呼叫,此時會將資料複製到一個臨時的 DirectByteBuffer
物件中,JDK 會為每個執行緒快取這個臨時物件,且不限制記憶體大小。因此,如果程式有多個執行緒生成很多大 HeapByteBuffer
物件時,且執行緒一直存活,則會導致程序會佔用大量的本地記憶體,造成記憶體洩露。
程式碼重現
// TODO
解決
- JDK 9提供了 JVM 引數:
-Djdk.nio.maxCachedBufferSize=262144
,來限制這個快取的大小; - 直接使用
DirectByteBuffer
,或考慮Netty
等 NIO 框架。