起因
运行在docker上的一个服务,在某个版本之后,占用的内存开始增长,直到docker分配的内存上限,但是并不会OOM。使用ps查看进程使用的内存和虚拟内存 ( Linux内存管理 )。除了虚拟内存比较高达到17GB以外,实际使用的内存RSS也夸张的达到了7GB,远远超过了-Xmx的设定。
[root]$ ps -p 75 -o rss,vsz
RSS VSZ 7152568 17485844
排查过程
明显的,是有堆外内存的使用,了解到基础软件涉及到netty,netty会用到一些DirectByteBuffer,第一轮排查采用如下方式:
jmap -dump:format=b,file=75.dump 75 通过分析堆内存找到DirectByteBuffer的引用和大小
部署一个升级基础软件之前的版本,持续观察
结果还是出现了内存超用的问题。
pmap
为了进一步分析问题,使用pmap查看进程的内存分配,通过RSS升序序排列。结果发现除了地址000000073c800000上分配的3GB堆以外,还有数量非常多的64M一块的内存段,还有巨量小的物理内存块映射到不同的虚拟内存段上。但到现在为止,不知道里面的内容是什么,是通过什么产生的。
[root]$ pmap -x 75 | sort -n -k3
.....省略N行
0000000040626000 55488 55484 55484 rwx-- [ anon ]
00007fa07c000000 65536 55820 55820 rwx-- [ anon ]
00007fa044000000 65536 55896 55896 rwx-- [ anon ]
00007fa0c0000000 65536 56304 56304 rwx-- [ anon ]
00007f9db8000000 65536 56360 56360 rwx-- [ anon ]
00007fa0b8000000 65536 56836 56836 rwx-- [ anon ]
00007fa084000000 65536 57916 57916 rwx-- [ anon ]
00007f9ec4000000 65532 59752 59752 rwx-- [ anon ]
00007fa008000000 65536 60012 60012 rwx-- [ anon ]
000000073c800000 3119140 2488596 2487228 rwx-- [ anon ]
total kB 17629516 7384476 7377520
应用程序大量申请64M大内存块的原因是由Glibc的一个版本升级引起的,通过export MALLOC_ARENA_MAX=4
可以解决VSZ占用过高的问题。虽然这也是一个问题,但却不是想要的,因为增长的是物理内存,而不是虚拟内存。
NMT
幸运的是 JDK1.8有Native Memory Tracker可以帮助定位。通过在启动参数上加入-XX:NativeMemoryTracking=detail
就可以启用。在命令行执行jcmd可查看内存分配。
#jcmd 75 VM.native_memory summary
虽然pmap得到的内存地址和NMT大体能对的上,但仍然有不少内存去向成谜。虽然是个好工具但问题并不能解决。
perf
使用 perf record -g -p 55 开启监控栈函数调用。运行一段时间后Ctrl+C结束,会生成一个文件perf.data。执行perf report -i perf.data查看报告。发现进程大量执行bzip相关函数。搜索zip,结果如下:
进程调用了Java_java_util_zip_Inflater_inflatBytes() 申请了内存,仅有一小部分调用Deflater释放内存。与pmap内存地址相比对,确实是bzip在搞鬼。
解决
java项目搜索zip定位到代码,发现确实有相关bzip压缩解压操作,而且GZIPInputStream有个地方没有close。
GZIPInputStream使用Inflater申请堆外内存,Deflater释放内存,调用close()方法来主动释放。如果忘记关闭,Inflater对象的生命会延续到下一次GC。在此过程中,堆外内存会一直增长。
原代码:
public byte[] decompress ( byte[] input) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copy(new GZIPInputStream(new ByteArrayInputStream(input)), out);
return out.toByteArray();
}
修改后:
public byte[] decompress(byte[] input) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(input));
IOUtils.copy(gzip, out);
gzip.close();
return out.toByteArray();
}
经观察,问题解决。