起因

  运行在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();
    }

  经观察,问题解决。