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
00000000d54aa000  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=653853KB, 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的简单的对应关系。希望对大家有些用处。文中如有错误,还望读者不吝赐教。

没有更多推荐了,返回首页