Java 进程在64位linux下占用巨大内存的分析

我们的一个系统上线后发现内存占用非常高,已分配内存达到11G,而已分配地址空间更是17G了,而根据jmap执行结果发现:

实际Java程序只用了1.4G内存,Xmx配置的是4096M,那么理论上应该只有4G多一点的RSS,继续使用pmaps分析:

这里发现一个规律,65488 + 48 = 65536, 65508 + 28 = 65536, 65496 + 40 = 65536,进程内有大量的这种64M的内存块,至于内容是什么,dump出来一下看看:

然后看下文件内容,发现里面很多都是HTTP的请求和响应,第一反映可能是我们用的jetty出现内存泄漏了,在Jetty/Howto/Prevent Memory Leaks 一文中提到了 Direct ByteBuffers 可能造成内存泄漏,因为jetty使用的NIO会大量用到Direct ByteBuffer,于是继续分析,看JDK里Direct ByteBuffer的代码,发现了几个问题
1)每次分配的时候都会修改 java.nio.Bits 里reservedMemory, totalCapacity, count等数据,因此可以通过查看这几个字段来发现Direct ByteBuffer用了多少,根据这几个字段的值得知我们的系统使用的Direct ByteBuffer只用了17M,因此罪魁祸首不是这一块。
2)Jetty写入数据的时候会间接调用 sun.nio.ch.Util.getTemporaryDirectBuffer 这个函数来分配一个临时的DirectBuffer,而这里会将DirectBuffer部分缓存到一个线程局部对象上,那么就分析下线程数量和这些内存块的关系,通过jstack发现系统只用了60多个线程(真够多的,该瘦身了),这些线程中可能会被IO操作用到的线程只有一半不到,而64M的内存块有大概:

如果这些算排除了是我们系统本身的问题,那其他的系统呢,通过调查发现其他几个java进程也有类似的问题,但是相对要好很多(那些系统并没有对外暴露),HTTP请求和并发量也要低好几个数量级,接下来我在谷歌上搜索“java huge memory usage 64m” 发现别人也遇到过这个问题,在帖子What consumes memory in java process? 里有人提到这可能是个glibc的问题,后来在我本地做压力测试,使用一个perl脚本模拟大量用户操作,压力压到比产品服务器高几个数量级,而我本地内存占用从没超过2G,对比产品服务器,我的电脑是debian 7.2,自带的glibc版本是2.17,而产品服务器是CentOS 6.4, glibc是2.12,版本差这么多,那么至少是不能排除glibc的问题,于是根据Linux glibc >= 2.10 (RHEL 6) malloc may show excessive virtual memory usage的建议,在测试服务器的启动脚本里增加了:

对比增加这个和不增加这个的差别,发现比较明显的内存用量差别,那么这次就肯定是glibc的问题了,关于这个问题,是RHEL6(和Centos 6.4同源)里glibc采用了新的arena内存分配算法来提高多进程应用的内存分配性能,glibc里相比老的实现多个进程共用一个堆,新实现里可以保证每个线程都有一个堆,这样避免内存分配时需要额外的加锁来降低性能,而上面的环境变量则可以配置进程里的glibc使用指定数量的arena堆,避免分配过多的堆导致过多的内存使用,而根据glibc的代码,一个64位进程最多arena堆数是 8 × CPU核数。每个堆的大小可以从arena.c中看到:

DEFAULT_MMAP_THRESHOLD_MAX是系统定义的,其值可以man mallopt查到定义:

The lower limit for this parameter is 0. The upper limit is DEFAULT_MMAP_THRESHOLD_MAX: 512*1024 on 32-bit systems or 4*1024*1024*sizeof(long) on 64-bit systems.

因此64位系统上这个HEAP_MAX_SIZE的值就是 2 * 4*1024*1024*sizeof(long) = 64M了
问题的根源算找到了,是RHEL6(CentOS6)自带的glibc引起的,解决方法就是用上面的环境变量来控制最大的arena堆数目,或者选用由Google开发的更适合多线程环境下的tcmalloc,安装好后在启动脚本里加上

然后趁这次部署重启几个java服务,奇迹出现了,内存用量比原来少了4G左右,不过依然还是很大,下一次部署的时候需要做一次tcmalloc的heap profile看问题出在哪里。


Last modified on 2014-03-31