雙11期間服務突發(fā)異常,我是如何緊急排查進程內存虛高、容器OOM問題的?
今天給大家分享一篇在實際工作過程中K8S Pod 容器內 Java 進程內存分析,內存虛高以及容器 OOM 和 Java OOM 問題定位的實戰(zhàn)案例,全程實戰(zhàn),好了,不多說了,進入正題。
案例背景
一個 K8S Pod,里面只有一個 Java 進程,K8S request 和 limit memory 都是 2G,Java 進程核心參數包括:-XX:+UseZGC -Xmx1024m -Xms768m。
服務啟動一段時間后,查看 Grafana 監(jiān)控數據,Pod 內存使用量約 1.5G,JVM 內存使用量約 500M,通過 jvm dump 分析沒有任何大對象,運行三五天后出現 K8S Container OOM。
首先區(qū)分下 Container OOM 和 Jvm OOM,Container OOM 是 Pod 內進程申請內存大約 K8S Limit 所致。
問題來了:
- Pod 2G 內存,JVM 設置了
Xmx 1G,已經預留了 1G 內存,為什么還會 Container OOM,這預留的 1G 內存被誰吃了。 - 正常情況下(無 Container OOM),Grafana 看到的監(jiān)控數據,Pod 內存使用量 1.5G, JVM 內存使用量 500M,差別為什么這么大。
- Pod 內存使用量為什么超過 Xmx 限制。
Grafana 監(jiān)控圖。
圖片
統計指標
Pod 內存使用量統計的指標是 container_memory_working_set_bytes:
- container_memory_usage_bytes = container_memory_rss + container_memory_cache + kernel memory
- container_memory_working_set_bytes = container_memory_usage_bytes - total_inactive_file(未激活的匿名緩存頁)
container_memory_working_set_bytes 是容器真實使用的內存量,也是資源限制 limit 時的 OOM 判斷依據。
另外注意 cgroup 版本差異: container_memory_cache reflects cache (cgroup v1) or file (cgroup v2) entry in memory.stat.
JVM 內存使用量統計的指標是 jvm_memory_bytes_used: heap、non-heap 以及其他 真實用量總和。下面解釋其他。
首先說結論:在 POD 內,通過 top、free 看到的指標都是不準確的,不用看了,如果要看真實的數據以 cgroup 為準。
container_memory_working_set_bytes 指標來自 cadvisor,cadvisor 數據來源 cgroup,可以查看以下文件獲取真實的內存情況。
# cgroup v2 文件地址
ll /sys/fs/cgroup/memory.*
-r--r--r-- 1 root root 0 Jan 6 16:25 /sys/fs/cgroup/memory.current
-r--r--r-- 1 root root 0 Jan 6 16:25 /sys/fs/cgroup/memory.events
-r--r--r-- 1 root root 0 Jan 6 16:25 /sys/fs/cgroup/memory.events.local
-rw-r--r-- 1 root root 0 Jan 7 11:50 /sys/fs/cgroup/memory.high
-rw-r--r-- 1 root root 0 Jan 7 11:50 /sys/fs/cgroup/memory.low
-rw-r--r-- 1 root root 0 Jan 7 11:50 /sys/fs/cgroup/memory.max
-rw-r--r-- 1 root root 0 Jan 6 16:25 /sys/fs/cgroup/memory.min
-r--r--r-- 1 root root 0 Jan 6 16:25 /sys/fs/cgroup/memory.numa_stat
-rw-r--r-- 1 root root 0 Jan 7 11:50 /sys/fs/cgroup/memory.oom.group
-rw-r--r-- 1 root root 0 Jan 6 16:25 /sys/fs/cgroup/memory.pressure
-r--r--r-- 1 root root 0 Jan 6 16:25 /sys/fs/cgroup/memory.stat
-r--r--r-- 1 root root 0 Jan 6 16:25 /sys/fs/cgroup/memory.swap.current
-r--r--r-- 1 root root 0 Jan 6 16:25 /sys/fs/cgroup/memory.swap.events
-rw-r--r-- 1 root root 0 Jan 6 16:25 /sys/fs/cgroup/memory.swap.high
-rw-r--r-- 1 root root 0 Jan 6 16:25 /sys/fs/cgroup/memory.swap.maxJVM 關于使用量和提交量的解釋。
Used Size:The used space is the amount of memory that is currently occupied by Java objects. 當前實際真的用著的內存,每個 bit 都對應了有值的。
Committed Size:The committed size is the amount of memory guaranteed to be available for use by the Java virtual machine. 操作系統向 JVM 保證可用的內存大小,或者說 JVM 向操作系統已經要的內存。站在操作系統的角度,就是已經分出去(占用)的內存,保證給 JVM 用了,其他進程不能用了。 由于操作系統的內存管理是惰性的,對于已申請的內存雖然會分配地址空間,但并不會直接占用物理內存,真正使用的時候才會映射到實際的物理內存,所以 committed > res 也是很可能的。
Java 進程內存分析
Pod 的內存使用量 1.5G,都包含哪些。
kernel memory 為 0,Cache 約 1100M,rss 約 650M,inactive_file 約 200M。可以看到 Cache 比較大,因為這個服務比較特殊有很多文件操作。
# cgroup v2 變量變了
cat /sys/fs/cgroup/memory.stat
anon 846118912
file 2321530880
kernel_stack 10895360
pagetables 15523840
percpu 0
sock 1212416
shmem 1933574144
file_mapped 1870290944
file_dirty 12288
file_writeback 0
swapcached 0
anon_thp 0
file_thp 0
shmem_thp 0
inactive_anon 2602876928
active_anon 176771072
inactive_file 188608512
active_file 199348224
unevictable 0
slab_reclaimable 11839688
slab_unreclaimable 7409400
slab 19249088
workingset_refault_anon 0
workingset_refault_file 318
workingset_activate_anon 0
workingset_activate_file 95
workingset_restore_anon 0
workingset_restore_file 0
workingset_nodereclaim 0
pgfault 2563565
pgmajfault 15
pgrefill 14672
pgscan 25468
pgsteal 25468
pgactivate 106436
pgdeactivate 14672
pglazyfree 0
pglazyfreed 0
thp_fault_alloc 0
thp_collapse_alloc 0通過 Java 自帶的 Native Memory Tracking 看下內存提交量。
# Java 啟動時先打開 NativeMemoryTracking,默認是關閉的。注意不要在生產環(huán)境長期開啟,有性能損失
java -XX:NativeMemoryTracking=detail -jar
# 查看詳情
jcmd $(pgrep java) VM.native_memory detail scale=MB
# 查看 summary
jcmd $(pgrep java) VM.native_memory summary scale=MB
# 創(chuàng)建基線,然后分不同時間進行 diff 查看變化
jcmd $(pgrep java) VM.native_memory baseline
jcmd $(pgrep java) VM.native_memory detail.diff scale=MB
jcmd $(pgrep java) VM.native_memory summary.diff scale=MB通過 Native Memory Tracking 追蹤到的詳情大致如下,關注其中每一項 committed 值。
Native Memory Tracking:
(Omitting categories weighting less than 1MB)
Total: reserved=68975MB, committed=1040MB
- Java Heap (reserved=58944MB, committed=646MB)
(mmap: reserved=58944MB, committed=646MB)
- Class (reserved=1027MB, committed=15MB)
(classes #19551) #加載類的個數
( instance classes #18354, array classes #1197)
(malloc=3MB #63653)
(mmap: reserved=1024MB, committed=12MB)
( Metadata: )
( reserved=96MB, committed=94MB)
( used=93MB)
( waste=0MB =0.40%)
( Class space:)
( reserved=1024MB, committed=12MB)
( used=11MB)
( waste=1MB =4.63%)
- Thread (reserved=337MB, committed=37MB)
(thread #335) #線程的個數
(stack: reserved=336MB, committed=36MB)
(malloc=1MB #2018)
- Code (reserved=248MB, committed=86MB)
(malloc=6MB #24750)
(mmap: reserved=242MB, committed=80MB)
- GC (reserved=8243MB, committed=83MB)
(malloc=19MB #45814)
(mmap: reserved=8224MB, committed=64MB)
- Compiler (reserved=3MB, committed=3MB)
(malloc=3MB #2212)
- Internal (reserved=7MB, committed=7MB)
(malloc=7MB #31683)
- Other (reserved=18MB, committed=18MB)
(malloc=18MB #663)
- Symbol (reserved=19MB, committed=19MB)
(malloc=17MB #502325)
(arena=2MB #1)
- Native Memory Tracking (reserved=12MB, committed=12MB)
(malloc=1MB #8073)
(tracking overhead=11MB)
- Shared class space (reserved=12MB, committed=12MB)
(mmap: reserved=12MB, committed=12MB)
- Module (reserved=1MB, committed=1MB)
(malloc=1MB #4996)
- Synchronization (reserved=1MB, committed=1MB)
(malloc=1MB #2482)
- Metaspace (reserved=97MB, committed=94MB)
(malloc=1MB #662)
(mmap: reserved=96MB, committed=94MB)
- Object Monitors (reserved=8MB, committed=8MB)
(malloc=8MB #39137)先解釋下內存參數意義:
reserved:JVM 向操作系統預約的虛擬內存地址空間,僅是地址空間預留,不消耗物理資源,防止其他進程使用這段地址范圍,類似"土地規(guī)劃許可",但尚未建房。
committed:JVM 實際分配的物理內存(RAM + Swap),真實消耗系統內存資源。
虛擬地址空間
┌──────────────────────────────┐
│ reserved=16MB │ ← 整個預約區(qū)域
├───────────────┬──────────────┤
│ committed=13MB│ 未使用 3MB │ ← 實際使用的物理內存
└───────────────┴──────────────┘
圖片
- Heap Heap 是 Java 進程中使用量最大的一部分內存,是最常遇到內存問題的部分,Java 也提供了很多相關工具來排查堆內存泄露問題,這里不詳細展開。Heap 與 RSS 相關的幾個重要 JVM 參數如下: Xms:Java Heap 初始內存大小。(目前我們用的百分比控制,MaxRAMPercentage) Xmx:Java Heap 的最大大小。(InitialRAMPercentage) XX:+UseAdaptiveSizePolicy:是否開啟自適應大小策略。開啟后,JVM 將動態(tài)判斷是否調整 Heap size,來降低系統負載。
- Metaspace Metaspace 主要包含方法的字節(jié)碼,Class 對象,常量池。一般來說,記載的類越多,Metaspace 使用的內存越多。與 Metaspace 相關的 JVM 參數有: XX:MaxMetaspaceSize: 最大的 Metaspace 大小限制【默認無限制】 XX:MetaspaceSize=64M: 初始的 Metaspace 大小。如果 Metaspace 空間不足,將會觸發(fā) Full GC。 類空間占用評估,給兩個數字可供參考:10K 個類約 90M,15K 個類約 100M。 什么時候回收:分配給一個類的空間,是歸屬于這個類的類加載器的,只有當這個類加載器卸載的時候,這個空間才會被釋放。釋放 Metaspace 的空間,并不意味著將這部分空間還給系統內存,這部分空間通常會被 JVM 保留下來。 擴展:參考資料中的
Java Metaspace 詳解,這里完美解釋 Metaspace、Compressed Class Space 等。 - Thread NMT 中顯示的 Thread 部分內存與線程數與 -Xss 參數成正比,一般來說 committed 內存等于
Xss *線程數。 - Code JIT 動態(tài)編譯產生的 Code 占用的內存。這部分內存主要由-XX:ReservedCodeCacheSize 參數進行控制。
- Internal Internal 包含命令行解析器使用的內存、JVMTI、PerfData 以及 Unsafe 分配的內存等等。 需要注意的是,Unsafe_AllocateMemory 分配的內存在 JDK11 之前,在 NMT 中都屬于 Internal,但是在 JDK11 之后被 NMT 歸屬到 Other 中。
- Symbol Symbol 為 JVM 中的符號表所使用的內存,HotSpot 中符號表主要有兩種:SymbolTable 與 StringTable。 大家都知道 Java 的類在編譯之后會生成 Constant pool 常量池,常量池中會有很多的字符串常量,HotSpot 出于節(jié)省內存的考慮,往往會將這些字符串常量作為一個 Symbol 對象存入一個 HashTable 的表結構中即 SymbolTable,如果該字符串可以在 SymbolTable 中 lookup(SymbolTable::lookup)到,那么就會重用該字符串,如果找不到才會創(chuàng)建新的 Symbol(SymbolTable::new_symbol)。 當然除了 SymbolTable,還有它的雙胞胎兄弟 StringTable(StringTable 結構與 SymbolTable 基本是一致的,都是 HashTable 的結構),即我們常說的字符串常量池。平時做業(yè)務開發(fā)和 StringTable 打交道會更多一些,HotSpot 也是基于節(jié)省內存的考慮為我們提供了 StringTable,我們可以通過 String.intern 的方式將字符串放入 StringTable 中來重用字符串。
- Native Memory Tracking Native Memory Tracking 使用的內存就是 JVM 進程開啟 NMT 功能后,NMT 功能自身所申請的內存。
觀察上面幾個區(qū)域的分配,沒有明顯的異常。
NMT 追蹤到的 是 Committed,不一定是 Used,NMT 和 cadvisor 沒有找到必然的對應的關系。可以參考 RSS,cadvisor 追蹤到 RSS 是 650M,JVM Used 是 500M,還有大約 150M 浮動到哪里去了。
因為 NMT 只能 Track JVM 自身的內存分配情況,比如:Heap 內存分配,direct byte buffer 等。無法追蹤的情況主要包括:
- 使用 JNI 調用的一些第三方 native code 申請的內存,比如使用 System.Loadlibrary 加載的一些庫。
- 標準的 Java Class Library,典型的,如文件流等相關操作(如:Files.list、ZipInputStream 和 DirectoryStream 等)。主要涉及到的調用是 Unsafe.allocateMemory 和 java.util.zip.Inflater.init(Native Method)。
怎么追蹤 NMT 追蹤不到的其他內存,目前是安裝了 jemalloc 內存分析工具,他能追蹤底層內存的分配情況輸出報告。
通過 jemalloc 內存分析工具佐證了上面的結論,Unsafe.allocateMemory 和 java.util.zip.Inflater.init 占了 30%,基本吻合。
圖片
啟動 arthas 查看下類調用棧,在 arthas 里執(zhí)行以下命令:
# 先設置 unsafe true
options unsafe true
# 這個沒有
stack sun.misc.Unsafe allocateMemory
# 這個有
stack jdk.internal.misc.Unsafe allocateMemory
stack java.util.zip.Inflater inflate
# stack 經常追蹤不到,改用 profiler 輸出內存分配火焰圖
profiler start --event alloc --duration 600
profiler start --event Unsafe_AllocateMemory0 --duration 600通過上面的命令,能看到 MongoDB 和 netty 一直在申請使用內存。注意:早期的 mongodb client 確實有無法釋放內存的 bug,但是在我們場景,長期觀察會發(fā)現內存申請了逐漸釋放了,沒有持續(xù)增長?;氐介_頭的 ContainerOOM 問題,可能一個原因是流量突增,MongoDB 申請了更多的內存導致 OOM,而不是因為內存不釋放。
ts=2022-12-29 21:20:01;thread_name=ForkJoinPool.commonPool-worker-1;id=22;is_daemnotallow=true;priority=1;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@1d44bcfa
@jdk.internal.misc.Unsafe.allocateMemory()
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:125)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:332)
at sun.nio.ch.Util.getTemporaryDirectBuffer(Util.java:243)
at java.net.Socket$SocketOutputStream.write(Socket.java:1035)
at com.mongodb.internal.connection.SocketStream.write(SocketStream.java:99)
at com.mongodb.internal.connection.InternalStreamConnection.sendMessage(InternalStreamConnection.java:426)
at com.mongodb.internal.connection.UsageTrackingInternalConnection.sendAndReceive(UsageTrackingInternalConnection.java:99)
at com.mongodb.internal.connection.DefaultConnectionPool$PooledConnection.sendAndReceive(DefaultConnectionPool.java:444)
………………………………
at com.mongodb.MongoClientExt$1.execute(MongoClientExt.java:42)
………………………………另外,arthas 自帶的 profiler 有時候經常追蹤失敗,可以切換到原始的 async-profiler ,用他來追蹤“其他”內存分配比較有效。
總結 Java 進程內存占用:Total=heap + non-heap + 上面說的這個其他。
jemalloc
jemalloc 是一個比 glibc malloc 更高效的內存池技術,在 Facebook 公司被大量使用,在 FreeBSD 和 FireFox 項目中使用了 jemalloc 作為默認的內存管理器。使用 jemalloc 可以使程序的內存管理性能提升,減少內存碎片。
比如 Redis 內存分配默認使用的 jemalloc,早期版本安裝 redis 是需要手動安裝 jemalloc 的,現在 redis 應該是在編譯期內置好了。
原來使用 jemalloc 是為了分析內存占用,通過 jemalloc 輸出當前內存分配情況,或者通過 diff 分析前后內存差,大概能看出內存都分給睡了,占了多少,是否有內存無法釋放的情況。
后來參考了這個文章,把 glibc 換成 jemalloc 帶來性能提升,降低內存使用,決定一試。
how we’ve reduced memory usage without changing any code:https://blog.malt.engineering/java-in-k8s-how-weve-reduced-memory-usage-without-changing-any-code-cbef5d740ad
Decreasing RAM Usage by 40% Using jemalloc with Python & Celery: https://zapier.com/engineering/celery-python-jemalloc/
一個服務,運行一周,觀察效果。
使用 Jemalloc 之前:
圖片
使用 Jemalloc 之后(同時調低了 Pod 內存):
圖片
注:以上結果未經生產長期檢驗。
內存交還給操作系統
注意:下面的操作,生產環(huán)境不建議這么干。
默認情況下,OpenJDK 不會主動向操作系統退還未用的內存(不嚴謹)??吹谝粡埍O(jiān)控的圖,會發(fā)現運行一段時間后,Pod 的內存使用量一直穩(wěn)定在 80%–90%不再波動。
其實對于 Java 程序,浮動比較大的就是 heap 內存。其他區(qū)域 Code、Metaspace 基本穩(wěn)定
# 執(zhí)行命令獲取當前 heap 情況
jhsdb jmap --heap --pid $(pgrep java)
#以下為輸出
Attaching to process ID 7, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 17.0.5+8-LTS
using thread-local object allocation.
ZGC with 4 thread(s)
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 1287651328 (1228.0MB)
NewSize = 1363144 (1.2999954223632812MB)
MaxNewSize = 17592186044415 MB
OldSize = 5452592 (5.1999969482421875MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 22020096 (21.0MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
ZHeap used 310M, capacity 710M, max capacity 1228MJava 內存不交還,幾種情況:
- Xms 大于實際需要的內存,比如我們服務設置了 Xms768M,但是實際上只需要 256,高峰期也就 512,到不了 Xms 的值也就無所謂歸還。
圖片
- 上面 jmap 的結果,可以看到 Java 默認的配置 MaxHeapFreeRatio=70,這個 70% Free 幾乎很難達到。(另外注意 Xmx==Xms 的情況下這兩個參數無效,因為他怎么擴縮都不會突破 Xms 和 Xmx 的限制)
MinHeapFreeRatio = 40
空閑堆空間的最小百分比,計算公式為:HeapFreeRatio =(CurrentFreeHeapSize/CurrentTotalHeapSize) * 100,值的區(qū)間為 0 到 100,默認值為 40。如果 HeapFreeRatio < MinHeapFreeRatio,則需要進行堆擴容,擴容的時機應該在每次垃圾回收之后。
MaxHeapFreeRatio = 70
空閑堆空間的最大百分比,計算公式為:HeapFreeRatio =(CurrentFreeHeapSize/CurrentTotalHeapSize) * 100,值的區(qū)間為 0 到 100,默認值為 70。如果 HeapFreeRatio > MaxHeapFreeRatio,則需要進行堆縮容,縮容的時機應該在每次垃圾回收之后。對于 ZGC,默認是交還給操作系統的。可通過 -XX:+ZUncommit -XX:ZUncommitDelay=300 這兩個參數控制(不再使用的內存最多延遲 300s 歸還給 OS,線下環(huán)境可以改小點)。
經過調整后的服務,內存提交在 500–800M 之間浮動,不再是一條直線。
圖片
內存分析工具速覽
使用 Java 自帶工具 dump 內存。
jcmd <pid> GC.heap_dump <file-path>
# 這個命令執(zhí)行,JVM 會先觸發(fā) gc,然后再統計信息。
jmap -dump:live,format=b,file=/opt/tomcat/logs/dump.hprof <pid>
# dump all
jmap -dump:format=b,file=/opt/tomcat/logs/dump.hprof <pid>使用 jmap 輸出內存占用概覽。
jmap -histo 1 | head -n 500使用 async-profiler 追蹤 native 內存分配,輸出火焰圖。
async-profiler/bin/asprof -d 600 -e Unsafe_AllocateMemory0 -f /opt/tomcat/logs/unsafe_allocate.html <pid>使用 vmtouch 查看和清理 Linux 系統文件緩存。
# 查看文件或文件夾占了多少緩存
vmtouch /files
vmtouch /dir
# 遍歷文件夾輸出詳細占用
vmtouch -v /dir
# 清空緩存
vmtouch -e /dirpmap 查看內存內容
使用 pmap 查看當前內存分配,如果找到了可疑的內存塊,可以通過 gdb 嘗試解析出內存塊中的內容。
# pmap 查看內存,先不要排序,便于找出連續(xù)的內存塊(一般是 2 個一組)
# pmap -x <pid> | sort -n -k3
pmap -x $(pgrep java)
# 舉例說明在上面發(fā)現有 7f6737dff000 開頭的內存塊可能異常,一般都是一個或多個一組,是連續(xù)的內存
cat /proc/$(pgrep java)/smaps > logs/smaps.txt
gdb attach $(pgrep java)
# dump 的起始地址,基于上面 smaps.txt 找到的內容,地址加上 0x 前綴
dump memory /opt/tomcat/logs/gdb-test.dump 0x7f6737dff000 0x7f6737e03000
# 嘗試將 dump 文件內容轉成可讀的 string,其中 -10 是過濾長度大于 10 的,也可以不過濾
strings -10 /opt/tomcat/logs/gdb-test.dump
# 如果幸運,能在上面的 strings 中找到你的 Java 類或 Bean 內容,如果不幸都是一堆亂碼,可以嘗試擴大 dump 內存塊,多找?guī)讉€連續(xù)的塊試試
# pmap 按大小降序排序并過濾大于 1000 KB 的項
pmap -x $(pgrep java) | awk 'NR>2 && !/total/ {print $2, $0}' | sort -k1,1nr | cut -d' ' -f2- | awk '$2 > 1000'識別 Linux 節(jié)點上的 cgroup 版本
cgroup 版本取決于正在使用的 Linux 發(fā)行版和操作系統上配置的默認 cgroup 版本。 要檢查你的發(fā)行版使用的是哪個 cgroup 版本,請在該節(jié)點上運行 stat -fc %T /sys/fs/cgroup/ 命令:
stat -fc %T /sys/fs/cgroup/對于 cgroup v2,輸出為 cgroup2fs。
對于 cgroup v1,輸出為 tmpfs。
問題原因分析和調整
回到開頭問題,通過上面分析,2G 內存,RSS 其實占用 600M,為什么最終還是 ContainerOOM 了。
- kernel memory 為 0,排除 kernel 泄漏的原因。下面的參考資料里介紹了 kernel 泄露的兩種場景。
- Cache 很大,說明文件操作多。搜了一下代碼,確實有很多 InputStream 調用沒有顯式關閉,而且有的 InputSteam Root 引用在 ThreadLocal 里,ThreadLocal 只 init 未 remove。 但是,ThreadLocal 的引用對象是線程池,池不回收,所以這部分可能會無法關閉,但是不會遞增,但是 cache 也不能回收。 優(yōu)化辦法:ThreadLocal 中對象是線程安全的,無數據傳遞,直接干掉 ThreadLocal;顯式關閉 InputStream。運行一周發(fā)現 cache 大約比優(yōu)化前低 200–500M。 ThreadLocal 引起內存泄露是 Java 中很經典的一個場景,一定要特別注意。
- 一般場景下,Java 程序都是堆內存占用高,但是這個服務堆內存其實在 200-500M 之間浮動,我們給他分了 768M,從來沒有到過這個值,所以調低 Xms。留出更多內存給 JNI 使用。
- 線下環(huán)境內存分配切換到 jemalloc,長期觀察大部分效果可以,但是對部分應用基本沒有效果。
經過上述調整以后,線下環(huán)境 Pod 內存使用量由 1G 降到 600M 作用。線上環(huán)境內存使用量在 50%–80%之間根據流量大小浮動,原來是 85% 居高不小。
不同 JVM 參數內存占用對比
以下為少量應用實例總結出來的結果,應用的模型不同占用情況會有比較大差異,僅供對比參考。
基礎參數 | 中低流量時內存占用(Xmx 6G) | 高流量時內存占用 |
Java 8 + G1 | 65% | 85% |
Java 17 + G1 | 60% | 75% |
Java 17 + ZGC | 90% | 95% |
Java 21 + G1 | 40% | 60% |
Java 21 + ZGC | 80% | 90% |
Java 21 + ZGC + UseStringDeduplication | 85% | 90% |
Java 21 + ZGC + ZGenerational + UseStringDeduplication | 75% | 80% |
總結:
- G1 比 ZGC 占用內存明顯減少。
- Java 21 比 Java 8、17 占用內存明顯偏少。
- Java 21 ZGC 分代后確實能降低內存。
- 通過
-XX:+UseStringDeduplication啟用 String 去重后,有的應用能降低 10% 內存,有的幾乎無變化。
分享我們所使用的 Java 21 生產環(huán)境參數配置,僅供參考請根據自己應用情況選擇性使用:
- -XX:InitialRAMPercentage=40.0 -XX:MaxRAMPercentage=70.0:按照百分比設置初始化和最大堆內存。內存充足的情況下建議設置為一樣大。
- -XX:+UseZGC -XX:+ZUncommit -XX:ZUncommitDelay=300 -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=30:促進 Java 內存更快交還給操作系統,但同時 CPU 可能偏高。
- -XX:+ZGenerational:啟用分代 ZGC,能降低內存占用。
- -XX:+UseStringDeduplication:啟用 String 去重,可能降低內存占用。
- -Xss256k:降低線程內存占用,默認 1Mb,線程比較多的情況下這個占用還是很多的。謹慎設置。
- -XX:+ParallelRefProcEnabled:多線程并行處理 Reference,減少 GC 的 Reference 數量,減少 Young GC 時間。
關于 Java 8、17 和 21 不同 GC 更多維度的對比效果可參考: https://kstefanj.github.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html 。
Java 分析工具
- [推薦] 在線 GC 分析工具 https://gceasy.io
- [推薦] 在線 Thread 分析工具 https://fastthread.io
- [推薦] 在線 Heap 分析工具 https://heaphero.io
- [推薦] 在線 jstack 分析工具 https://jstack.review
- [Beta] 可私有化部署 Online GC、Heap Dump、Thread、JFR 分析工具 https://github.com/eclipse/jifa
參考資料
IOTDB 線上堆外內存泄漏問題排查:https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=195728187
JVM 堆外內存問題定位: https://juejin.cn/post/6844904168549777421
java 堆外內存泄漏排查: https://javakk.com/1158.html




























