1. 程式人生 > >Java記憶體之本地記憶體分析神器: NMT 和 pmap

Java記憶體之本地記憶體分析神器: NMT 和 pmap

背景

其他團隊的一些同事經常問我一個問題:你的Java程序怎麼佔了那麼多Virtual Size和RSS? 最近,我基本上可以回答清楚這個問題了。用NMT和pmap基本就就能搞清楚Java程序為什麼佔了那些Virtual Size和RSS。 NMT是Native Memory Tracking的縮寫,是Java7U40引入的HotSpot新特性。 pmap,眾所周知,就是Linux上用來看程序地址空間的。

結論

開門見山,我們先說分析結果,在pmap的輸出中,如下兩條就包含了Java的Heap空間。

START               SIZE     RSS     PSS   DIRTY          SWAP PERM MAPPING
00000000
d54aa000 92824K 92824K 92824K 92824K 0K rw-p [anon] 00000000daf50000 174784K 174784K 174784K 174784K 0K rw-p [anon]

從NMT的輸出中,我們可以得出地址空間[0xd5a00000, 0xe5a00000]正是對應了Java的Heap。0xd5a00000正好位於上面pmap輸出的第一條記錄中,0xe5a00000正好位於上面pmap輸出的第二條記錄中。更詳細的對應關係如下圖所示,

JavaNativeMemory

如圖所示, C到E就是對應到Java Heap; C到D就是對應到新生代(左邊的S0C, S1C, EC, OC是jstat -gc的輸出);D到E就是對應到老年代。圖片底部給出了一些很有意思的計算結果。希望這個分析結果對你來說也用處。

下面我們分三步去分析清楚Java是如何佔用這些底層作業系統的記憶體的。(以下討論,假定要分析的Java程式的程序號為14179,其實這個程序號就是一個Tomcat執行例項,上面執行著我的應用。)

NMT的輸出

首先,你要在Java啟動項中,加入啟動項: -XX:NativeMemoryTracking=detail 然後,重新啟動Java程式。執行如下命令: jcmd 14179 VM.native_memory detail 你會在標準輸出得到類似下面的內容(本文去掉了許多與本文無關或者重複的資訊):

14179:

Native Memory Tracking:

Total: reserved=653853
KB, committed=439409KB - Java Heap (reserved=262144KB, committed=262144KB) (mmap: reserved=262144KB, committed=262144KB) - Class (reserved=82517KB, committed=81725KB) (classes #17828) (malloc=1317KB #26910) (mmap: reserved=81200KB, committed=80408KB) - Thread (reserved=20559KB, committed=20559KB) (thread #58) (stack: reserved=20388KB, committed=20388KB) (malloc=102KB #292) (arena=69KB #114) - Code (reserved=255309KB, committed=41657KB) (malloc=5709KB #11730) (mmap: reserved=249600KB, committed=35948KB) - GC (reserved=1658KB, committed=1658KB) (malloc=798KB #676) (mmap: reserved=860KB, committed=860KB) - Compiler (reserved=130KB, committed=130KB) (malloc=31KB #357) (arena=99KB #3) - Internal (reserved=5039KB, committed=5039KB) (malloc=5007KB #20850) (mmap: reserved=32KB, committed=32KB) - Symbol (reserved=18402KB, committed=18402KB) (malloc=14972KB #221052) (arena=3430KB #1) - Native Memory Tracking (reserved=2269KB, committed=2269KB) (malloc=53KB #1597) (tracking overhead=2216KB) - Arena Chunk (reserved=187KB, committed=187KB) (malloc=187KB) - Unknown (reserved=5640KB, committed=5640KB) (mmap: reserved=5640KB, committed=5640KB) . . . Virtual memory map: [0xceb00000 - 0xcec00000] reserved 1024KB for Class from [0xced00000 - 0xcee00000] reserved 1024KB for Class from . . . [0xcf85e000 - 0xcf8af000] reserved and committed 324KB for Thread Stack from [0xd4eaf000 - 0xd4f00000] reserved and committed 324KB for Thread Stack from [0xf687866e] Thread::record_stack_base_and_size()+0x1be [0xf68818bf] JavaThread::run()+0x2f [0xf67541f9] java_start(Thread*)+0x119 [0xf7606395] start_thread+0xd5 [0xd5a00000 - 0xe5a00000] reserved 262144KB for Java Heap from . . . [0xe5e00000 - 0xf4e00000] reserved 245760KB for Code from [0xf737f000 - 0xf7400000] reserved 516KB for GC from [0xf745d000 - 0xf747d000] reserved 128KB for Unknown from [0xf7700000 - 0xf7751000] reserved and committed 324KB for Thread Stack from [0xf7762000 - 0xf776a000] reserved and committed 32KB for Internal from

上面的輸出也就兩大部分:Total和Virtual Memory Map. Total部分就是Java程序所使用的本地記憶體大小的一個分佈: Heap用了多少,所有的Class用了多少。其中,最重要的一個就是Heap大小,此處它的Reserved值為262144KB, 其實也就是256MB, 因為該Java啟動引數最大堆設為了256M:-Xmx256M。 Virtual Memory Map部分就是細節了,也就是Java程序的地址空間的每一段是用來幹什麼的,大小是多少。這些程序空間段按照用途分可以分為以下幾種:
 Reserved for Class (總共有76段)
例如:[0xceb00000 - 0xcec00000] reserved 1024KB for Class from
[0xced00000 - 0xcee00000] reserved 1024KB for Class from
大部分的為Class分配的程序空間都是1024KB的。
 Reserved for Heap ( 總共只有1段)
例如:[0xd5a00000 - 0xe5a00000] reserved 262144KB for Java Heap from
簡單演算一下:0xe5a00000-0xd5a00000=0x10000000=pow(2, 28)。很明顯2的28方個位元就是256MB.
 Reserved for Internal (總共只有1段)
例如:[0xf7762000 - 0xf776a000] reserved and committed 32KB for Internal from
 Reserved for Thread Stack(總共有57段)
例如:[0xcf85e000 - 0xcf8af000] reserved and committed 324KB for Thread Stack from
從輸出看,大部分的 Stack的地址空間都是324KB的,還有不少部分是516KB的。
 Reserved for Code( 總共有2段 )
例如:[0xe5e00000 - 0xf4e00000] reserved 245760KB for Code from
這個地方用了好大的程序空間。後面,我們會在pmap的輸出中找到它。它用了很大的Virtual Address Space, 但是RSS卻相對比較小。
 Reserved for Unknown( 總共有4 段)
例如: [0xf745d000 - 0xf747d000] reserved 128KB for Unknown from
 Reserved for GC (總共有2段)
例如: [0xf737f000 - 0xf7400000] reserved 516KB for GC from

pmap的輸出

使用命令列: pmap -p PID, 我們就可以得到對應程序的VSS&RSS資訊。
pmap輸出的中,我們把其中我們比較關心的部分列在下面:

START               SIZE     RSS     PSS   DIRTY    SWAP PERM MAPPING
0000000008048000      4K      4K      4K      0K      0K r-xp /usr/java/jre1.8.0_65/bin/java
0000000008049000      4K      4K      4K      4K      0K rw-p /usr/java/jre1.8.0_65/bin/java
000000000804a000  74348K  71052K  71052K  71052K      0K rw-p [heap]
…
00000000ced00000   1024K    976K    976K    976K      0K rw-p [anon]
…
00000000d4eaf000     12K      0K      0K      0K      0K ---p [anon]
00000000d4eb2000    312K     28K     28K     28K      0K rwxp [stack:21151]
00000000d4f00000   1024K   1024K   1024K   1024K      0K rw-p [anon]
00000000d5000000     32K     32K     32K      0K      0K r-xp /usr/java/jre1.8.0_65/jre/lib/i386/libmanagement.so
00000000d5008000      4K      4K      4K      4K      0K rw-p /usr/java/jre1.8.0_65/jre/lib/i386/libmanagement.so
00000000d500d000    324K     24K     24K     24K      0K rwxp [stack:18608]
00000000d505e000   4376K   4376K   4376K   4376K      0K rw-p [anon]
00000000d54a4000     24K      0K      0K      0K      0K ---p [anon]
00000000d54aa000  92824K  92824K  92824K  92824K      0K rw-p [anon]
00000000daf50000 174784K 174784K 174784K 174784K      0K rw-p [anon]
00000000e5a40000    544K    544K    544K    544K      0K rw-p [anon]
00000000e5ac8000   3296K      0K      0K      0K      0K ---p [anon]
00000000e5e00000  34656K  34300K  34300K  34300K      0K rwxp [anon]
00000000e7fd8000 211104K      0K      0K      0K      0K ---p [anon]
00000000f4e00000    100K     60K     60K      0K      0K r-xp /usr/java/jre1.8.0_65/jre/lib/i386/libzip.so
00000000f4e19000      4K      4K      4K      4K      0K rw-p /usr/java/jre1.8.0_65/jre/lib/i386/libzip.so
00000000f4e5e000    648K     68K     68K     68K      0K rwxp [stack:18331]
00000000f4f00000   1024K   1024K   1024K   1024K      0K rw-p [anon]
…
Total:           735324K 482832K 479435K 462244K      0K

我們對幾個重要部分的pmap輸出一一作出分析,

  • 000000000804a000: 該部分是Java程序的Heap區,此處的Heap指的不是Java那種特殊的Heap, 還是一個任何C/C++記憶體區域中Heap區。VSS和RSS差不多,都在70M上下。
  • 00000000ced00000: 該區域就是用來存放class的。在NMT輸出中可以找到對應項。Mapping那一欄是[anon], 因為pmap並不知道這部分割槽域是幹什麼用的,而直有JVM自己知道,所以, NMT的輸出可以看出該記憶體區域的用處。
  • 00000000d4eaf000 00000000d4eb2000: 這兩部分合起來就是一個324K大小的Java Thread Stack。NTM輸出中可以找到對應項。
  • 00000000d54aa000, 00000000daf50000: 這兩部分就非常重要的。它對應就是我們Java意義上的堆的那一部分。簡單地講,- 00000000daf50000那一塊就是老年代(Java記憶體分佈分析要以垃圾收集演算法為前提)。00000000d54aa000這一部分包含了新生代。結合jstat –gc的輸出可以得出這個結論。 在下一小節,你就可以看到jstat –gc 的輸出

jstat -gc的輸出

/jstat -gc 14179
Picked up JAVA_TOOL_OPTIONS: -XX:-UseLargePages
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
8704.0 8704.0 2435.5  0.0   69952.0  29138.2   174784.0   146972.4  83736.0 82674.8  0.0    0.0      740    9.341  81      3.713   13.054

參照文章開頭給出的圖示,就可以把pmap的RSS和Java的Heap聯絡和對應起來。(注:筆者測試環境是Java 8, 所以你在jstat輸出中會看到MC和MU)。

結尾

以上,我們就得出了一些Java的VSS&RSS和Java Heap的簡單的對應關係。希望對大家有些用處。文中如有錯誤,還望讀者不吝賜教。