一次jvm导致线上内存占用过高问题定位
背景:8G物理內存,8核CPU,jvm使用的G1垃圾回收器。
問題:線上內存占用告警,內存占用超過85%,且現象一直持續。
分析
看一下jvm啟動參數配置:
-Xms6144m -Xmx6144m -Xss256k -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/tmp/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:+UseStringDeduplication -XX:GCLogFileSize=16M -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Duser.timezone=Asia/Shanghai -Dfile.encoding=UTF-8進容器內,觀察內存占用。
top free -h發現java進程占用了7G多的內存,但內存占用大小波動很小。第一想法是否有內存泄漏(回過頭看,想法是好的:內存占用高懷疑是否有內存泄漏。但其實已經走了誤區,內存占用穩定應當去定位java進程占用的內存被誰占用了。堆內存、元空間還是堆外內存?堆內的話是年輕代還是老年代?但還是記錄一下這次jvm問題定位排查的過程),去查看堆內存的實際內存占用。
先看一下java進程pid、jmap命令路徑:
ps -ef | grep java find / -name jmap然后,執行jmap命令查看堆內存概覽:
堆信息概覽: jmap -heap pid 堆中對象概覽: jmap -histo pid | head -50?例如我實際執行的:
/data/services/jdk8u161/bin/jmap -heap 376 /data/services/jdk8u161/bin/jmap -histo 376 | head -50?堆內存分配了6G,實際占用800M,而java進程占用了7G,其它內存被誰占用了?首先想到的是堆外內存,是否是堆外內存的溢出導致的?
元空間的觀察
jvm GC信息,間隔一秒打印一次: jstat -gc pid 1000 或者 jstat -gcutil pid 1000 S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 0.0 96256.0 0.0 96256.0 1644544.0 1105920.0 4550656.0 138468.4 114280.0 109400.0 13688.0 12775.8 14 1.211 0 0.000 1.2110.0 96256.0 0.0 96256.0 1644544.0 1105920.0 4550656.0 138468.4 114280.0 109400.0 13688.0 12775.8 14 1.211 0 0.000 1.211 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 100.00 89.54 3.04 95.73 93.34 14 1.211 0 0.000 1.2110.00 100.00 89.66 3.04 95.73 93.34 14 1.211 0 0.000 1.211?MC(元空間分配):114280.0K,MU(元空間使用):109400.0K。元空間使用了100M多一點,排除。但為防止元空間的內存溢出,JVM啟動參數中建議添加參數:-XX:MaxMetaspaceSize=256M
堆外內存的觀察
為了觀察java進程堆外內存的占用,JVM啟動參數中添加參數:-XX:NativeMemoryTracking=summary,這個參數對jvm可能會有5%左右的性能損耗,所以生產環境不推薦開啟。
同時,-XX:+DisableExplicitGC:禁止顯示GC,即代碼中聲明的? System.gc();//建議jvm進行gc? 不再生效。在jdk源碼中使用nio申請堆外內存時,堆外內存不足時會執行?System.gc() 進行堆外內存的回收,所以,堆外內存使用較多時不推薦配置?-XX:+DisableExplicitGC。
最后,為了防止堆外內存的溢出,jvm啟動參數建議添加:-XX:MaxDirectMemorySize=1024M
修改后的jvm啟動參數配置如下:
-Xms6144m -Xmx6144m -Xss256k -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:MaxDirectMemorySize=1024M -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/tmp/gc-%t.log -Duser.timezone=Asia/Shanghai -Dfile.encoding=UTF-8jcmd 追蹤java本地內存
配置好后,重新啟動容器,jcmd 追蹤java進程本地內存:
查看java進程內存占用詳細情況(-XX:NativeMemoryTracking=summary,關閉NMT命令:jcmd pid VM.native_memory shutdown): jcmd pid VM.native_memory scale=MB保存java進程內存占用情況的基準版本: jcmd pid VM.native_memory scale=MB baseline與基準版本進行比較(若懷疑存在內存泄漏,可過段時間再執行觀察): jcmd pid VM.native_memory scale=MB summary.diff我實際執行的命令:?
/data/services/jdk8u161/bin/jcmd 376 VM.native_memory scale=MB/data/services/jdk8u161/bin/jcmd 376 VM.native_memory scale=MB baseline/data/services/jdk8u161/bin/jcmd 376 VM.native_memory scale=MB summary.diff對比2張圖,可發現內存波動很小。Java Heap(堆內存)分配了6G,實際也占用了6G。堆外內存實際占用了800多M,主要被Class(元空間)、Thread(java線程棧,含GC本地線程)、Code(本地字節碼,即JIT存儲熱點代碼地方)、GC(JVM GC額外占用的,例如G1中的Remembered Set等數據結構)、Internal(Direct Buffer直接內存,例如nio)等占用。Native Memory Tracking表示該功能自身占用的部分。
一段時間后,再次對比發現內存波動還是很小,這時候才反應過來,是不是不存在內存泄漏,只不過是java進程真的占用了這么高的內存。那為什么java進程內存占用一直這么高呢?仔細看一下上圖,發現這一行:?Java Heap (reserved=6144MB, committed=6144MB) ,JVM為java堆保留了6G,堆實際也占用了6G,才回想起來JVM的啟動參數配置中:-Xms6144m -Xmx6144m,自己設置了最大堆為6G,再加上堆外內存的800多M,那java進程實際不就是占用7G左右嘛。
解決
修改-Xms6144m -Xmx6144m為-Xms4096m -Xmx4096m,物理內存的一半,重啟后,容器的內存占用最后穩定在60%左右,內存占用過高的問題沒有再次出現。最后的JVM啟動參數配置為:
-Xms4096m -Xmx4096m -Xss256k -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:MaxDirectMemorySize=1024M -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/tmp/gc-%t.log -Duser.timezone=Asia/Shanghai -Dfile.encoding=UTF-8思考
為何jvm堆實際占用穩定在6G?我嘗試手動進行了full GC,馬上再去觀察,發現堆內存實際占用還是6G。可是jmap -heap pid觀察堆信息概覽,看到heap used實際只有幾十M,那這是為什么呢?為什么JVM沒有將內存歸還給操作系統呢?可參考博客:https://segmentfault.com/a/1190000040050819
總結:JVM 的垃圾回收,只是一個邏輯上的回收,回收的只是 JVM 申請的那一塊邏輯堆區域,將數據標記為空閑之類的操作,不是調用 free 將內存歸還給操作系統。
JVM 還是會歸還內存給操作系統的,只是因為這個代價比較大,所以不會輕易進行。而且不同垃圾回收器的內存分配算法不同,歸還內存的代價也不同。或者說歸還內存給操作系統的操作并沒有那么簡單,執行起來代價過高,JVM 自然不會在每次 GC 后都進行內存的歸還。
JVM問題定位使用到的命令
top free -h ps -ef | grep java find / -name jmap 堆信息概覽: jmap -heap pid 堆中對象概覽: jmap -histo pid | head -50 jvm GC信息: jstat -gc pid 1000 jstat -gcutil pid 1000 查看java進程內存占用詳細情況(-XX:NativeMemoryTracking=summary,關閉NMT命令:jcmd pid VM.native_memory shutdown): jcmd pid VM.native_memory scale=MBFull GC后顯示存活對象: jmap -histo:live pid | head -20 查看jvm啟動參數: jinfo -flags pid or jps -v Full GC后dump堆棧文件: jmap -dump:format=b,live,file=heapdump.hprof pid保存java進程內存占用情況的基準版本: jcmd pid VM.native_memory scale=MB baseline 與基準版本進行比較(若懷疑存在內存泄漏,可過段時間再執行觀察): jcmd pid VM.native_memory scale=MB summary.diff總結
以上是生活随笔為你收集整理的一次jvm导致线上内存占用过高问题定位的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 实现AlphaGo(二):快速构建棋盘和
- 下一篇: java9 javascript_[Ja