從前面幾篇文章,我們了解了 NMT 的基礎(chǔ)知識(shí)以及 NMT 追蹤區(qū)域分析的相關(guān)內(nèi)容,本篇文章將為大家介紹一下使用 NMT 協(xié)助排查內(nèi)存問(wèn)題的案例。
6.使用 NMT 協(xié)助排查內(nèi)存問(wèn)題案例
我們?cè)诟闱宄?NMT 追蹤的 JVM 各部分的內(nèi)存分配之后,就可以比較輕松的協(xié)助排查定位內(nèi)存問(wèn)題或者調(diào)整合適的參數(shù)。
可以在 JVM 運(yùn)行時(shí)使用 jcmd
比如我們看到 MetaSpace 的內(nèi)存增長(zhǎng)異常,可以結(jié)合 MAT 等工具查看是否類加載器數(shù)量異常、是否類重復(fù)加載、reflect 的 inflation 參數(shù)設(shè)置是否合理;如果 Symbol 內(nèi)存增長(zhǎng)異常,可以查看項(xiàng)目 String.intern 是否使用正常;如果 Thread 使用內(nèi)存過(guò)多,考慮是否可以適當(dāng)調(diào)整線程堆棧大小等等。
案例一:虛高的 VIRT 內(nèi)存
我們還記得前文(NMT 內(nèi)存 & OS 內(nèi)存概念差異性章節(jié))中使用 top 命令查看啟動(dòng)的 JVM 進(jìn)程,仔細(xì)觀察會(huì)發(fā)現(xiàn)一個(gè)比較虛高的 VIRT 內(nèi)存(10.7g),我們使用 NMT 追蹤的 Total: reserved 才 2813709KB(2.7g),這多出來(lái)的這么多虛擬內(nèi)存是從何而來(lái)呢?
top PIDUSERPRNIVIRTRESSHRS%CPU%MEMTIME+COMMAND 27420douyiwa+20010.7g69756017596S100.00.30:18.79java NativeMemoryTracking: Total:reserved=2813077KB,committed=1496981KB
使用pmap -X
27420:java-Xmx1G-Xms1G-XX:+UseG1GC-XX:MaxMetaspaceSize=256M-XX:MaxDirectMemorySize=256M-XX:ReservedCodeCacheSize=256M-XX:NativeMemoryTracking=detail-jarnmtTest.jar AddressPermOffsetDeviceInodeSizeRssPssReferencedAnonymousLazyFreeShmemPmdMappedShared_HugetlbPrivate_HugetlbSwapSwapPssLockedMapping c0000000rw-p0000000000:00010490886372366372366372366372360000000 100080000---p0000000000:000104806400000000000 aaaaea835000r-xp00000000fd:0245613083444400000000java aaaaea854000r--p0000f000fd:0245613083444440000000java aaaaea855000rw-p00010000fd:0245613083444440000000java aaab071af000rw-p0000000000:0003041081081081080000000[heap] fffd60000000rw-p0000000000:00013244440000000 fffd60021000---p0000000000:0006540400000000000 fffd68000000rw-p0000000000:00013288880000000 fffd68021000---p0000000000:0006540400000000000 fffd6c000000rw-p0000000000:00013244440000000 fffd6c021000---p0000000000:0006540400000000000 fffd70000000rw-p0000000000:000132404040400000000 fffd70021000---p0000000000:0006540400000 ......
可以發(fā)現(xiàn)多了很多 65404 KB 的內(nèi)存塊(大約 120 個(gè)),使用 /proc/
...... fffd60021000-fffd64000000---p0000000000:000 Size:65404kB KernelPageSize:4kB MMUPageSize:4kB Rss:0kB Pss:0kB Shared_Clean:0kB Shared_Dirty:0kB Private_Clean:0kB Private_Dirty:0kB Referenced:0kB Anonymous:0kB LazyFree:0kB AnonHugePages:0kB ShmemPmdMapped:0kB Shared_Hugetlb:0kB Private_Hugetlb:0kB Swap:0kB SwapPss:0kB Locked:0kB VmFlags:mrmwmenr ......
對(duì)照 NMT 的情況,我們發(fā)現(xiàn)如 fffd60021000-fffd64000000 這種 65404 KB 的內(nèi)存是并沒(méi)有被 NMT 追蹤到的。這是因?yàn)樵?JVM 進(jìn)程中,除了 JVM 進(jìn)程自己 mmap 的內(nèi)存(如 Java Heap,和用戶進(jìn)程空間的 Heap 并不是一個(gè)概念)外,JVM 還直接使用了類庫(kù)的函數(shù)來(lái)分配一些數(shù)據(jù),如使用 Glibc 的 malloc/free (也是通過(guò) brk/mmap 的方式):
既然 JVM 使用了 Glibc 的 malloc/free,就不得不提及 malloc 的機(jī)制,早期版本的 malloc 只有一個(gè) arena(分配區(qū)),每次分配時(shí)都要對(duì)分配區(qū)加鎖,分配完成之后再釋放,這就導(dǎo)致了多線程的情況下競(jìng)爭(zhēng)比較激烈。
所以 malloc 改動(dòng)了其分配機(jī)制,甚至有了 arena per-thread 的模式,即如果在一個(gè)線程中首次調(diào)用 malloc,則創(chuàng)建一個(gè)新的 arena,而不是去查看前面的鎖是否會(huì)發(fā)生競(jìng)爭(zhēng),對(duì)于一定數(shù)量的線程可以避免競(jìng)爭(zhēng)在自己的 arena 上工作。
arena 的數(shù)量限制在 32 位系統(tǒng)上是 2 * CPU 核心數(shù),64 位系統(tǒng)上是 8 * CPU 核心數(shù),當(dāng)然我們也可以使用 MALLOC_ARENA_MAX (Linux 環(huán)境變量,詳情可以查看 mallopt(3)[1])來(lái)控制。
查看發(fā)現(xiàn)運(yùn)行 JVM 進(jìn)程的環(huán)境 CPU 信息(物理 CPU 核數(shù)):Core(s) per socket: 64 。
我們給當(dāng)前環(huán)境設(shè)置 MALLOC_ARENA_MAX=2,重啟 JVM 進(jìn)程,查看使用情況:
top PIDUSERPRNIVIRTRESSHRS%CPU%MEMTIME+COMMAND 36319douyiwa+200310834069087217828S100.00.30:07.61java
虛高的 VIRT 內(nèi)存已經(jīng)降下來(lái)了,繼續(xù)查看 pmap/smaps 會(huì)發(fā)現(xiàn)眾多的 65404 KB 的內(nèi)存空間也消失了(120 * 65404 KB = 7848480 KB 正好對(duì)應(yīng)了 10.7g - 3108340 KB 的內(nèi)存,即 VIRT 降低的內(nèi)存)。
為什么我們的 JVM 進(jìn)程會(huì)使用如此多的 arena 呢?因?yàn)槲覀冊(cè)趩?dòng) JVM 進(jìn)程的時(shí)候,并沒(méi)有手動(dòng)去設(shè)置一些進(jìn)程的數(shù)目,如:CICompilerCount(編譯線程數(shù))、ConcGCThreads/ParallelGCThreads(并發(fā) GC 線程數(shù))、G1ConcRefinementThreads(G1 Refine 線程數(shù))等等。
這些參數(shù)大多數(shù)根據(jù)當(dāng)前機(jī)器的 CPU 核數(shù)去計(jì)算默認(rèn)值,使用 jinfo -flags
-XX:CICompilerCount=18 -XX:ConcGCThreads=11 -XX:G1ConcRefinementThreads=43
這些線程數(shù)目都是比較大的,我們也可以不修改 MALLOC_ARENA_MAX 的數(shù)量,而通過(guò)參數(shù)減小線程的數(shù)量來(lái)減少 arena 的數(shù)量。
Glibc 的 malloc 有時(shí)會(huì)出現(xiàn)碎片問(wèn)題,可以使用 jemalloc/tcmalloc 等替代 Glibc。
案例二:堆外內(nèi)存的排查
有時(shí)候我們會(huì)發(fā)現(xiàn),Java 堆、MetaSpace 等區(qū)域是比較正常的,但是 JVM 進(jìn)程整體的內(nèi)存卻在不停的增長(zhǎng),此時(shí)我們就可以使用 NMT 的 baseline & diff 功能來(lái)觀察究竟是哪塊區(qū)域內(nèi)存一直增長(zhǎng)。
比如在一次案例中發(fā)現(xiàn):
NativeMemoryTracking: Total:reserved=8149334KB+1535794KB,committed=6999194KB+1590490KB ...... -Internal(reserved=1723321KB+1472458KB,committed=1723321KB+1472458KB) (malloc=1723289KB+1472458KB#109094+47573) (mmap:reserved=32KB,committed=32KB) ...... [0x00007fceb806607a]Unsafe_AllocateMemory+0x17a [0x00007fcea1d24e68] (malloc=1485579KBtype=Internal+1455929KB#2511+2277) ......
我們可以確認(rèn)內(nèi)存 1590490KB 的增長(zhǎng),基本上都是由 Internal 的 Unsafe_AllocateMemory 所分配的,此時(shí)可以優(yōu)先考慮 NIO 中 ByteBuffer.allocateDirect / DirectByteBuffer / FileChannel.map 等使用方式是不是出現(xiàn)了泄漏,可以使用 MAT 查看 DirectByteBuffer 對(duì)象的數(shù)量是否異常,并可以使用 -XX:MaxDirectMemorySize 來(lái)限制 Direct 的大小。
設(shè)置 -XX:MaxDirectMemorySize 之后,進(jìn)程異常的內(nèi)存增長(zhǎng)停止,但是 GC 頻率變高,查看 GC 日志發(fā)現(xiàn):.887+0800: 238210.127: [Full GC (System.gc()) 1175M->255M(3878),0.8370418 secs]。
FullGC 的頻率大大增加,并且基本上都是由 System.gc() 顯式調(diào)用引起的(HotSpot中的System.gc()為 FulGC),查看 DirectByteBuffer 相關(guān)邏輯:
#DirectByteBuffer.java DirectByteBuffer(intcap){//package-private ...... Bits.reserveMemory(size,cap); longbase=0; try{ base=unsafe.allocateMemory(size); }catch(OutOfMemoryErrorx){ Bits.unreserveMemory(size,cap); throwx; } unsafe.setMemory(base,size,(byte)0); if(pa&&(base%ps!=0)){ //Rounduptopageboundary address=base+ps-(base&(ps-1)); }else{ address=base; } cleaner=Cleaner.create(this,newDeallocator(base,size,cap)); att=null; } #Bits.java staticvoidreserveMemory(longsize,intcap){ ...... System.gc(); ...... }
DirectByteBuffer 在 unsafe.allocateMemory(size) 之前會(huì)先去做一個(gè) Bits.reserveMemory(size, cap) 的操作,Bits.reserveMemory 會(huì)顯式的調(diào)用 System.gc() 來(lái)嘗試回收內(nèi)存,看到這里基本可以確認(rèn)為 DirectByteBuffer 的問(wèn)題,排查業(yè)務(wù)代碼,果然發(fā)現(xiàn)一處 ByteBufferStream 使用了 ByteBuffer.allocateDirect 的方式而流一直未關(guān)閉釋放內(nèi)存,修正后內(nèi)存增長(zhǎng)與 GC 頻率皆恢復(fù)正常。
審核編輯:劉清
-
JVM
+關(guān)注
關(guān)注
0文章
158瀏覽量
12238 -
LINUX內(nèi)核
+關(guān)注
關(guān)注
1文章
316瀏覽量
21672 -
NMT
+關(guān)注
關(guān)注
0文章
7瀏覽量
3647
原文標(biāo)題:Native Memory Tracking 詳解(4):使用 NMT 協(xié)助排查內(nèi)存問(wèn)題案例
文章出處:【微信號(hào):openEulercommunity,微信公眾號(hào):openEuler】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論