https://zhuanlan.zhihu.com/p/669599050
背景
今天遇到了線上環境的容器服務當機現象,懷疑是記憶體洩漏導致被docker kill。借用Native Memory Tracker工具檢視記憶體使用情況。
Native Memory Tracking 概念
JVM中的本地記憶體追蹤NMT: Native Memory Tracking。
這個是官方提供的一個檢視 JVM 記憶體佔用的工具引入。不過要注意的一點是,這個只能監控 JVM 原生申請的記憶體大小,如果是透過 JDK 封裝的系統 API 申請的記憶體,是統計不到的,例如 Java JDK 中的 DirectBuffer 以及 MappedByteBuffer 這兩個。以及封裝 JNI 呼叫系統呼叫去申請記憶體,都是 Native Memory Tracking 無法涵蓋的。
Native Memory Tracking 的開啟
Native Memory Tracking 預設是不開啟的,並且無法動態開啟(因為NMT的實現方式是埋點採集統計,如果可以動態開啟那麼沒開啟的時候的記憶體分配沒有記錄無法知曉,所以無法動態開啟),目前只能透過在啟動 JVM 的時候透過啟動引數開啟。即透過 -XX:NativeMemoryTracking
開啟:
-XX:NativeMemoryTracking=off|summary|detail
注意:啟用NMT會導致5% -10%的效能開銷。
-XX:NativeMemoryTracking=off
:這是預設值,即關閉 Native Memory Tracking-XX:NativeMemoryTracking=summary
: 開啟 Native Memory Tracking,但是僅僅按照各個 JVM 子系統去統計記憶體佔用情況-XX:NativeMemoryTracking=detail
:開啟 Native Memory Tracking,從每次 JVM 中申請記憶體的不同呼叫路徑的維度去統計記憶體佔用情況。注意,開啟 detail 比開啟 summary 的消耗要大不少,因為 detail 每次都要解析 CallSite 分辨呼叫位置。
Native Memory Tracking 的使用
開啟之後,可以透過 jcmd 命令去檢視 Native Memory Tracking 的即時快照資訊,即jcmd <pid> VM.native_memory
。
jcmd <pid> VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]
jcmd <pid> VM.native_memory
或者jcmd <pid> VM.native_memory summary
:兩者是等價的,即檢視 Native Memory Tracking 的 summary 資訊。預設單位是 KB,可以指定單位為其他,例如jcmd <pid> VM.native_memory summary scale=MB
jcmd <pid> VM.native_memory detail
:檢視 Native Memory Tracking 的 detail 資訊,包括 summary 資訊,以及按照虛擬記憶體對映分組的記憶體使用資訊,還有按照不同 CallSite 呼叫分組的記憶體使用情況。預設單位是 KB,可以指定單位為其他,例如jcmd <pid> VM.native_memory detail scale=MB
假設應用的 VMOption 配置如下:
-XX:NativeMemoryTracking=summary -Xms10240m -Xmx10240m -XX:+UseG1GC
如何獲取 pid?
為了找到一個JVM應用程式的PID,我們使用jps命令: jps -l
分析NMT summary 資訊組成
$ jcmd 19544 VM.native_memory summary scale=MB
19544:
Native Memory Tracking:
(Omitting categories weighting less than 1MB)
Total: reserved=12491MB, committed=10954MB
malloc: 121MB #730024
mmap: reserved=12370MB, committed=10833MB
- Java Heap (reserved=10240MB, committed=10240MB)
(mmap: reserved=10240MB, committed=10240MB)
- Class (reserved=1026MB, committed=15MB)
(classes #19552)
( instance classes #18434, array classes #1118)
(malloc=2MB #49338)
(mmap: reserved=1024MB, committed=12MB)
( Metadata: )
( reserved=96MB, committed=89MB)
( used=88MB)
( waste=1MB =0.88%)
( Class space:)
( reserved=1024MB, committed=12MB)
( used=12MB)
( waste=1MB =5.72%)
- Thread (reserved=341MB, committed=34MB)
(thread #341)
(stack: reserved=340MB, committed=33MB)
(malloc=1MB #2048)
- Code (reserved=245MB, committed=37MB)
(malloc=3MB #12204)
(mmap: reserved=242MB, committed=34MB)
- GC (reserved=451MB, committed=451MB)
(malloc=38MB #14413)
(mmap: reserved=412MB, committed=412MB)
- Compiler (reserved=1MB, committed=1MB)
(malloc=1MB #1126)
- Internal (reserved=3MB, committed=3MB)
(malloc=3MB #53489)
- Other (reserved=34MB, committed=34MB)
(malloc=34MB #170)
- Symbol (reserved=21MB, committed=21MB)
(malloc=19MB #557798)
(arena=2MB #1)
- Native Memory Tracking (reserved=11MB, committed=11MB)
(tracking overhead=11MB)
- Shared class space (reserved=16MB, committed=12MB)
(mmap: reserved=16MB, committed=12MB)
- Arena Chunk (reserved=3MB, committed=3MB)
(malloc=3MB)
- Tracing (reserved=32KB, committed=32KB)
(arena=32KB #1)
- Logging (reserved=5KB, committed=5KB)
(malloc=5KB #216)
- Arguments (reserved=31KB, committed=31KB)
(malloc=31KB #90)
- Module (reserved=1690KB, committed=1690KB)
(malloc=1690KB #6811)
- Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB)
- Synchronization (reserved=500KB, committed=500KB)
(malloc=500KB #4576)
- Serviceability (reserved=2MB, committed=2MB)
(malloc=2MB #31062)
- Metaspace (reserved=97MB, committed=90MB)
(malloc=1MB #517)
(mmap: reserved=96MB, committed=89MB)
- String Deduplication (reserved=1KB, committed=1KB)
(malloc=1KB #8)
- Object Monitors (reserved=1248KB, committed=1248KB)
(malloc=1248KB #6143)
上述輸出中有些項轉為MB會為0. 因此寫為KB。
逐行分析,將上面的資訊按不同子系統分別簡單分析下其含義:
- Java Heap
- Class
- Thread
- Code
- GC
- Compiler
- Internal
- Other
- Symbol
- Native Memory Tracking
- Shared class space
- Arena Chunk
- Tracing
- Logging
- Arguments
- Module
- Safepoint
- Synchronization
- Serviceability
- Metaspace
- String Deduplication
- Object Monitors
1. Total: reserved=12491MB, committed=10954MB
輸出資訊:
Total: reserved=12491MB, committed=10954MB
// 透過 malloc 方式分配 121MB
malloc: 121MB #730024
// 透過 mmap 方式分配的情況
mmap: reserved=12370MB, committed=10833MB
這個是全部的保留和使用的記憶體。保留記憶體顯示了我們應用程式能夠使用的全部記憶體。使用記憶體是當前正在使用的記憶體。
儘管分配了10954MB的堆記憶體,但是我們應用程式全部的保留記憶體大約12491MB。
2. Java Heap
Java堆記憶體,所有 Java 物件分配佔用記憶體的來源,由 JVM GC 管理回收。
// 堆記憶體佔用,reserve 了 10240MB,當前 commit 了 10240MB 用於實際使用。
// 保留和使用記憶體的實際大小符合我們的設定。
Java Heap (reserved=10240MB, committed=10240MB)
// 堆記憶體都是透過 mmap 系統呼叫方式分配的
(mmap: reserved=10240MB, committed=10240MB)
3. Metaspace
元空間,JVM 將類檔案載入到記憶體中用於後續使用佔用的空間,注意是 JVM C++ 層面的記憶體佔用,主要包括類檔案中在 JVM 解析為 C++ 的 Klass 類以及相關元素。對應的 Java 反射類 Class 還是在堆記憶體空間中。
大約1026MB的保留和15MB的使用空間區,載入19552個類。
// Class 是類元空間總佔用,reserve 了 1026MB,當前 commit 了 15MB 用於實際使用
// 總reserved 構成:總reserved 1026MB = mmap reserved 1024MB + malloc 2MB
// 總committed 構成:總 committed 15MB = mmap committed 12MB + malloc 2MB(單位到kb更準確。單位在mb會有四捨五入)
Class (reserved=1026MB, committed=15MB)
(classes #19552) // 一共載入了 19552 個類
( instance classes #18434, array classes #1118) // 其中 18434 個實體類,1118 個陣列類
(malloc=2MB #49338) // 透過 malloc 系統呼叫方式一共分配了 2MB,一共呼叫了 49338 次 malloc
(mmap: reserved=1024MB, committed=12MB) // 透過 mmap 系統呼叫方式 reserve 了 1024MB,當前 commit 了 12MB 用於實際使用
( Metadata: ) // MetaData 這塊不屬於類元空間,屬於資料元空間
( reserved=96MB, committed=89MB) // 資料元空間當前 reserve 了 96MB,commit 了 89MB 用於實際使用
( used=88MB) // 實際從 MetaChunk 的角度去看使用,只有 88MB 用於實際資料的分配,有 1MB 的浪費
( waste=1MB =0.88%)
( Class space:)
( reserved=1024MB, committed=12MB) // 類元空間當前 reserve 了 1024MB,commit 了 12MB 用於實際使用
( used=12MB) // 實際從 MetaChunk 的角度去看使用,12MB 用於實際資料的分配
( waste=1MB =5.72%)
// 共享類空間,當前 reserve 了 16MB,commit 了 12MB 用於實際使用,這塊其實屬於上面 Class 的一部分
Shared class space (reserved=16MB, committed=12MB)
(mmap: reserved=16MB, committed=12MB)
// 載入並記錄模組佔用空間,當前 reserve 了 1MB,commit 了 1MB 用於實際使用
Module (reserved=1MB, committed=1MB)
(malloc=1MB #6811)
// 等價於上面 Class 中的 MetaChunk(除了 malloc 的部分),
// 當前 reserve 了 97MB,commit 了 90MB 用於實際使用
Metaspace (reserved=97MB, committed=90MB)
(malloc=1MB #517)
(mmap: reserved=96MB, committed=89MB)
4. 符號 Symbol
C++ 字串即符號(Symbol)佔用空間,如同String table和常量池。
前面載入類的時候,其實裡面有很多字串資訊(注意不是 Java 字串,是 JVM 層面 C++ 字串),不同類的字串資訊可能會重複(維護原創打死潮汐犬)。所以統一放入符號表(Symbol table)複用。元空間中儲存的是針對符號表中符號的引用。
Symbol (reserved=21MB, committed=21MB)
// 透過 malloc 系統呼叫方式一共分配了 19MB,一共呼叫了 557798 次 malloc
(malloc=19MB #557798)
//透過 arena 系統呼叫方式一共分配了 2MB,一共呼叫了 1 次 arena
(arena=2MB #1)
5. 執行緒 Thread
執行緒佔用記憶體,主要是每個執行緒的執行緒棧。
// 總共 reserve 了 341MB,commit 了 34MB
Thread (reserved=341MB, committed=34MB)
(thread #341) // 當前執行緒數量是 341
// 執行緒棧佔用的空間:VMOption 沒有指定 Xss,預設是 1MB,所以 reserved 是 341 * 1024 = 340MB(還是單位MB問題),當前 commit 了 33MB 用於實際使用
(stack: reserved=340MB, committed=33MB)
(malloc=1MB #2048) // 透過 malloc 系統呼叫方式一共分配了 1MB,一共呼叫了 2048 次 malloc。 不存在透過 JVM 內部 Arena 分配的記憶體
6. Code Cache
JIT編譯器本身佔用的空間以及JIT編譯器編譯後的程式碼佔用空間。
// 當前,大約37MB的空間被會快取了,並且能使用的空間大約在245MB。
Code (reserved=245MB, committed=37MB)
(malloc=3MB #12204)
(mmap: reserved=242MB, committed=34MB)
Compiler (reserved=1MB, committed=1MB)
(malloc=1MB #1126)
7. Arena
Arena 資料結構佔用空間,我們看到 Native Memory Tracking 中有很多透過 arena 分配的記憶體,這個就是管理 Arena 資料結構佔用空間。
Arena Chunk (reserved=3MB, committed=3MB)
(malloc=3MB)
8. Native Memory Tracking
開啟 Native Memory Tracking 本身消耗的記憶體
Native Memory Tracking (reserved=11MB, committed=11MB)
(tracking overhead=11MB)
9. Serviceability
JVM TI 相關記憶體,JVMTI 是 Java 虛擬機器工具介面(Java Virtual Machine Tool Interface)的縮寫。它是 Java 虛擬機器(JVM)的一部分,提供了一組 API,使開發人員可以開發自己的 Java 工具和代理程式,以監視、分析和除錯 Java 應用程式。JVMTI API 是一組 C/C++ 函式,可以透過 JVM TI Agent Library 和 JVM 進行互動。開發人員可以使用 JVMTI API 開發自己的 JVM 代理程式或工具,以監視和操作 Java 應用程式。例如,可以使用 JVMTI API 開發效能分析工具、程式碼覆蓋率工具、記憶體洩漏檢測工具等等。這裡的記憶體就是呼叫了 JVMTI API 之後 JVM 為了生成資料佔用的記憶體。
Serviceability (reserved=2MB, committed=2MB)
(malloc=2MB #31062)
10. GC
JVM GC需要的資料結構與記錄資訊佔用的空間,這塊記憶體可能會比較大,尤其是對於那種專注於低延遲的 GC,例如 ZGC。其實 ZGC 是一種以空間換時間的思路,提高 CPU 消耗與記憶體佔用,但是消滅全域性暫停。之後的 ZGC 最佳化方向就是儘量降低 CPU 消耗與記憶體佔用,相當於提高了價效比。
GC (reserved=451MB, committed=451MB)
(malloc=38MB #14413)
(mmap: reserved=412MB, committed=412MB)
11. Internal、Other
JVM內部(不屬於其他類的佔用就會歸到這一類)與其他佔用(不是 JVM 本身而是作業系統的某些系統呼叫導致額外佔的空間)。 堆外記憶體的分配佔用也會在這裡的Internal部分有所體現。
Internal (reserved=3MB, committed=3MB)
(malloc=3MB #53489)
Other (reserved=34MB, committed=34MB)
(malloc=34MB #170)
12. String Deduplication
Java 字串去重機制可以減少應用程式中字串物件的記憶體佔用。如果開啟了此配置,則會出現此項:Java 字串去重佔用記憶體。
String Deduplication (reserved=1KB, committed=1KB)
(malloc=1KB #8)
在 Java 應用程式中,字串常量是不可變的,並且通常被使用多次。這意味著在應用程式中可能存在大量相同的字串物件,這些物件佔用了大量的記憶體。Java 字串去重機制透過在堆中共享相同的字串物件來解決這個問題。當一個字串物件被建立時,JVM 會檢查堆中是否已經存在相同的字串物件。如果存在,那麼新的字串物件將被捨棄,而引用被返回給現有的物件。這樣就可以減少應用程式中字串物件的數量,從而減少記憶體佔用。 但是這個機制一直在某些 GC 下表現不佳,尤其是 G1GC 以及 ZGC 中,預設是關閉的。
可以透過 -XX:+UseStringDeduplication
來啟用。
13. Tracing
JVM Tracing 佔用記憶體,包括 JVM perf 以及 JFR 佔用的空間。
Tracing (reserved=32KB, committed=32KB)
(arena=32KB #1)
14. Logging
寫 JVM 日誌佔用的記憶體(-Xlog
引數指定的日誌輸出,並且 Java 17 之後引入了非同步 JVM 日誌-Xlog:async
,非同步日誌所需的 buffer 也在這裡)
Logging (reserved=5KB, committed=5KB)
(malloc=5KB #216)
15. Arguments
JVM 引數佔用記憶體,我們需要儲存並處理當前的 JVM 引數以及使用者啟動 JVM 的是傳入的各種引數(有時候稱為 flag)。
Arguments (reserved=31KB, committed=31KB)
(malloc=31KB #90)
16. Safepoint
JVM 安全點佔用記憶體,是固定的兩頁記憶體(我這裡是一頁是 4KB,這個頁大小與作業系統相關),用於 JVM 安全點的實現,不會隨著 JVM 執行時的記憶體佔用而變化。
Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB)
17. Synchronization
Java 同步機制(例如synchronized
,還有 AQS 的基礎LockSupport
)底層依賴的 C++ 的資料結構,系統內部的 mutex 等佔用的記憶體。
Synchronization (reserved=500KB, committed=500KB)
(malloc=500KB #4576)
監控時間段
現在 JVM 一般大部分部署在 k8s 這種雲容器編排的環境中,每個 JVM 程序記憶體是受限的。如果超過限制,那麼會觸發 OOMKiller 將這個 JVM 程序殺掉。但 JVM 程序被 OOMKiller 時的記憶體使用分佈情況如何不得而知,這時可以考慮開啟 NativeMemoryTracking 根據不同系統模組的記憶體佔用做響應調整。
OOMKiller 是積分制,並不是 JVM 程序一超過限制就立刻會被殺掉,而是超過的話會累積分,累積到一定程度,就可能會被 OOMKiller 殺掉。所以,可以透過定時輸出 Native Memory Tracking 的 summary 資訊,從而抓到超過記憶體限制的點進行分析。
smaps_rollup
JVM 中所謂的 commit 記憶體,只是將記憶體mmaped對映為可讀可寫可執行的狀態!而在 Linux 中,在分配記憶體時又是 lazy allocation 的機制,只有在程序真正訪問時才分配真實的實體記憶體。所以 NMT 中所統計的 committed 並不是對應的真實的實體記憶體,因此我們不能僅透過 Native Memory Tracking 的資料就判斷 JVM 佔用的記憶體。同時,JVM 還會動態釋放一些記憶體,這些記憶體可能不會立刻被作業系統回收。Native Memory Tracking 是 JVM 認為自己向作業系統申請的記憶體,與實際作業系統分配的記憶體是有所差距的,因此不能體現真正記憶體佔用指標。
我們可以透過smaps_rollup(linux 程序監控檔案)輔助檢視具體的記憶體佔用。
- 一般不看 Rss指標,因為如果涉及多個虛擬地址對映同一個實體地址的話會有不準確
- 關注 Pss 即可,但是 Pss 更新不是實時的,可以理解為程序佔用的實際實體記憶體
命令: cat /proc/<pid>/smaps_rollup
$ cat /proc/19544/smaps_rollup
580000000-7fff6af2d000 ---p 00000000 00:00 0 [rollup]
Rss: 7782920 kB
Pss: 7759993 kB
Pss_Anon: 7742008 kB
Pss_File: 17985 kB
Pss_Shmem: 0 kB
Shared_Clean: 31332 kB
Shared_Dirty: 0 kB
Private_Clean: 9564 kB
Private_Dirty: 7742024 kB
Referenced: 7726536 kB
Anonymous: 7742008 kB
LazyFree: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
FilePmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 0 kB
監控時間段
NMT允許我們追蹤在一段時間內的記憶體改變情況。
首先標記當前應用程式的狀態作為基線:
$ jcmd <pid> VM.native_memory baseline
Baseline succeeded
一段時間後,比較出當前記憶體與基線之間的差別:
$ jcmd <pid> VM.native_memory summary.diff
現在,使用+和-標記,就能夠告訴我們在這段時間內記憶體的使用情況。
問題排查手段:虛擬機器退出時獲取 NMT 資料
上述一直在說JVM執行時獲取 NMT 資料。這裡聊聊異常退出時輸出NMT資料。
透過兩個引數:-XX:+UnlockDiagnosticVMOptions和-XX:+PrintNMTStatistics ,來獲取虛擬機器退出時記憶體使用情況的資料(輸出資料的詳細程度取決於你設定的跟蹤級別,如 summary/detail 等)。
- -XX:+UnlockDiagnosticVMOptions:解鎖用於診斷 JVM 的選項,預設關閉。
- -XX:+PrintNMTStatistics:當啟用 NMT 時,在虛擬機器退出時列印記憶體使用情況,預設關閉,需要開啟前置引數 -XX:+UnlockDiagnosticVMOptions才能正常使用。
此時,最終的VMOption為:
-XX:NativeMemoryTracking=summary -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics -Xms10240m -Xmx10240m -XX:+UseG1GC