背景
其他团队的一些同事经常问我一个问题:你的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输出的第二条记录中。更详细的对应关系如下图所示,
如图所示, 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:
- 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的简单的对应关系。希望对大家有些用处。文中如有错误,还望读者不吝赐教。
---------------------