好久不收拾,这里都长草了。先广告一下,弄了一个shawnma.com的域名(其实去年就申请了,然后就给忘了,浪费了50元啊!),以后shawnma.xomud.com就不搞了,xomud.com我也不打算续了,有人想要么,可以转让,哈。
这是一个很久以前写了一半的东西(从URL来看,去年11月),坚持写完:
—- 跟时髦的分割线 —
最近公司的产品出了一个奇怪的问题,有一个weblogic跑的应用,跑着跑着,就core dump,产生一个40G的core文件。一开始觉得很奇怪,为什么有这么大的一个core,一般就想,嗯,java内存泄露了。但是这个应用的-Xmx一共才4G,加上PermGen,加上JVM的东西,顶多也就5、6G的core啊。再说这个系统总共内存只有32G,40G的core一定是因为某些native library或者其他的什么地方泄露了内存。
Solaris有一些很好的工具,是Linux所没有的,一旦到了Linux平台我就很怀念他们。比如pstack,比如dtrace。尤其是dtrace,加上一些网上的dtrace script,用来troubleshooting真的是很无敌,跟Windows底下的Sysinternals suite一样。不知道是不是Linux也有类似的工具?
pstack可以用来检查一个core文件产生时系统所有线程的状态。(Java也有一个,jstack,有时候不太好用)不过由于我们是内存泄露,当然要用pmap(java有jmap,不过只能查看java heap)。一个正常的java经常的内存image应该是这样:
$ pmap 3536 |more 3536: /opt/middleware/jdk1.6.0_21/bin/amd64/java -Dweblogic.Name=imdmapi -se 0000000000400000 52K r-x-- /opt/middleware/jdk1.6.0_21/bin/amd64/java 000000000041C000 4K rw--- /opt/middleware/jdk1.6.0_21/bin/amd64/java 000000000041D000 158048K rw--- [ heap ] FFFFFD7ECBCD0000 152K rw--R [ anon ] FFFFFD7ECC2D6000 152K rw--R [ stack tid=2729 ] ...... FFFFFD7EEA6F1000 8K rw--- /opt/middleware/jdk1.6.0_21/jre/lib/amd64/libnet.so FFFFFD7EEA6F5000 8K r--s- dev:32,0 ino:477791 FFFFFD7EEA6F8000 16K r--s- dev:32,0 ino:8281559 FFFFFD7EEA7FB000 16K rw--R [ stack tid=8 ] FFFFFD7EEA800000 1048576K rw--- [ anon ] FFFFFD7F2A800000 3145728K rw--- [ anon ] FFFFFD7FEA800000 131072K rw--- [ anon ] FFFFFD7FF2800000 88064K rw--- [ anon ] .... FFFFFD7FFF3FD000 8K rwx-- /lib/amd64/ld.so.1 FFFFFD7FFFDFC000 16K rw--- [ stack ]
其中最上面低地址段的是真正的进程的Heap。底下那段红色的anonymous的内存段才是真正的java heap——它其实在内存高端,相当于跟栈在抢内存。这段内存不是用malloc()这样的东西来申请的,而是通过mmap()把一个文件映射到内存里来申请的。我们指定一个4G的max heap,那么JVM就mmap一个4G的文件。这个文件,当然就是/dev/zero。这个行为也是可以观察的,用truss(Linux: strace)指定跟踪mmap何open这两个系统调用就能发现了JVM先open了/dev/zero,然后把这个文件映射进内存。这样还有一个好处是内存相当于已经都memset成0了!至于为什么使用mmap,据说是为了产生一块连续的内存作为heap,不过我不明白为什么pmap显示它还是分成几段。
再来看看我们的core文件的map:
core 'core.2' of 12264: /opt/middleware/jdk1.6.0_21/bin/amd64/java -Dserver.name=iip -Xrs -Xms 0000000000400000 52K r-x-- /opt/middleware/jdk1.6.0_21/bin/amd64/java 000000000041C000 4K rw--- /opt/middleware/jdk1.6.0_21/bin/amd64/java 000000000041D000 2227600K rw--- 0000000088381000 2097152K rw--- 0000000108381000 4194304K rw--- 0000000208381000 8388608K rw--- 0000000408381000 16777216K rw--- [ heap ] FFFFFD7EDE76C000 88K rw--- [ stack tid=166 ] FFFFFD7EDE86D000 88K rw--- [ stack tid=165 ] …… FFFFFD7EF2800000 2048K rw--- FFFFFD7EF2A00000 2048K rw--- FFFFFD7EF2C00000 2048K rw--- FFFFFD7EF2E00000 2048K rw--- FFFFFD7EFAA00000 2797568K rw--- FFFFFD7FA5600000 1398784K rw--- FFFFFD7FFAC01000 4K r---- FFFFFD7FFAC03000 8K r----
hmm,进程的heap很大(16+8+4+2+2=32G),加上JVM的heap,差不多就39G了。JVM的heap还是4G,所以这绝不是java代码泄露,而是native的代码泄露了。另外,这些内存片似乎很有规律,2,2,4,8,16,32是不是有某些特殊的事情发生呢?先用truss看看是不是:
truss -t brk -p
嗯,想要在heap上(非JVM heap)申请内存一定要用brk函数,malloc/calloc只是libc的函数。brk我总觉得是bark,实际上是break的意思。anyway..brk的结果也显示,申请的内存恰好是2,4,8这样指数递增的。
开始有点怀疑是weblogic的某些lib有问题,因为weblogic为了性能使用了一些native的库。想过用valgrind,或者dmalloc之类的东西来分析native内存泄露,不过那似乎太麻烦了。好在我们有dtrace!比如下面的这个代码就可以在调用brk()这个函数的时候,打印出调用的stack出来:
#!/usr/sbin/dtrace -s syscall::brk:entry /pid == $1/ { stack(); printf("====>allocating to: %x\n", arg0); }
它的意思很简单,当调到syscall的brk这个函数的入口(entry)的时候,而且满足pid=<脚本参数>的情况下,打印stack,并且打印出arg0,也就是调brk的第一个函数。结果如下:
0 52693 brk:entry libc.so.1`_brk_unlocked+0xa libc.so.1`sbrk+0x20 libc.so.1`_morecore+0x12b libc.so.1`_malloc_unlocked+0x10d libc.so.1`malloc+0x2d libjvm.so`__1cCosGmalloc6FL_pv_+0x1e libjvm.so`__1cFChunk2n6FLL_pv_+0x49 libjvm.so`__1cFArenaEgrow6ML_pv_+0x3a libjvm.so`__1cINodeHashEgrow6M_v_+0x87 libjvm.so`__1cINodeHashQhash_find_insert6MpnENode__2_+0x140 libjvm.so`__1cMPhaseIterGVNNtransform_old6MpnENode__2_+0x731 libjvm.so`__1cMPhaseIterGVNIoptimize6M_v_+0xff libjvm.so`__1cHCompileIOptimize6M_v_+0x801 ....
每次的结果都是类似的,这里显然没有java的什么事,也不是weblogic的代码,而是JVM本身的代码。如果你看到里面有hash的字样,你应该联想到,2,4,8,16这样的事情应该是hash表每次rehash的时候把自己的bucket扩大一倍造成的。那么,这个hash表最后一共有十几个G bucket,显然是死循环造成的。仔细再看这个线程,是java的compiler thread,它似乎在优化什么东西。那么我们通过pstack来看看它。通过pstack很容易能看到目前进程里每个线程的状态,然后我们可以只看这个compiler thread:
pstack
就会看到这个compiler thread总是在运行,而且总是有这个NodeHash的字样在里面,显然死死循环了。
根据“你不会是第一个”的互联网原则,搜索了一下,发现有人报了这么一个JVM bug。直到我们用的JDK 6 u21里面还有这个bug,而且这已经是当时最新的JDK了!还好,过了1个月甲骨文公司出了6u23,fix了这个bug。还去看了一下,fix的办法就是如果这个optimizer搞了很多次,就return,不optimize了!
另外,JVM死掉的时候一般会有一个hs_xxx.log的文件,后来仔细看了一下,它其实已经记录了,当前的thread就是compiler thread,每次都是。所以如果早点看这个文件,也许可以少绕点弯子。