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

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

Attaching to process ID 1507, please wait...Debugger attached successfully.Server compiler detected.JVM version is 24.0-b56using thread-local object allocation.Parallel GC with 4 thread(s)Heap Configuration:   MinHeapFreeRatio = 40   MaxHeapFreeRatio = 70   MaxHeapSize      = 4831838208 (4608.0MB)   NewSize          = 1310720 (1.25MB)   MaxNewSize       = 17592186044415 MB   OldSize          = 5439488 (5.1875MB)   NewRatio         = 2   SurvivorRatio    = 8   PermSize         = 134217728 (128.0MB)   MaxPermSize      = 268435456 (256.0MB)   G1HeapRegionSize = 0 (0.0MB)Heap Usage:PS Young GenerationEden Space:   capacity = 957349888 (913.0MB)   used     = 861426704 (821.5205230712891MB)   free     = 95923184 (91.47947692871094MB)   89.9803420669539% usedFrom Space:   capacity = 326107136 (311.0MB)   used     = 266214288 (253.88172912597656MB)   free     = 59892848 (57.11827087402344MB)   81.63399650352945% usedTo Space:   capacity = 326631424 (311.5MB)   used     = 0 (0.0MB)   free     = 326631424 (311.5MB)   0.0% usedPS Old Generation   capacity = 689438720 (657.5MB)   used     = 395480112 (377.1592254638672MB)   free     = 293958608 (280.3407745361328MB)   57.36261984241326% usedPS Perm Generation   capacity = 134217728 (128.0MB)   used     = 55542368 (52.969329833984375MB)   free     = 78675360 (75.03067016601562MB)   41.38228893280029% used24587 interned Strings occupying 2169000 bytes.

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

00007fb870000000   65488   25100   25100 rw---    [ anon ]00007fb873ff4000      48       0       0 -----    [ anon ]00007fb874000000   65508   22560   22560 rw---    [ anon ]00007fb877ff9000      28       0       0 -----    [ anon ]00007fb878000000   65488   22772   22772 rw---    [ anon ]00007fb87bff4000      48       0       0 -----    [ anon ]00007fb87c000000   65496   62200   62200 rw---    [ anon ]00007fb87fff6000      40       0       0 -----    [ anon ]00007fb880000000   65516   65516   65516 rw---    [ anon ]00007fb883ffb000      20       0       0 -----    [ anon ]00007fb884000000   65488   65488   65488 rw---    [ anon ]00007fb887ff4000      48       0       0 -----    [ anon ]00007fb888000000   65492   65492   65492 rw---    [ anon ]00007fb88bff5000      44       0       0 -----    [ anon ]00007fb88c000000   65500   65500   65500 rw---    [ anon ]00007fb88fff7000      36       0       0 -----    [ anon ]

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

gdb --pid 1507(gdb) dump memory memory.bin 0x00007fb884000000 0x00007fb884000000+65488

然后看下文件内容,发现里面很多都是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的内存块有大概:

$ pmap -x 1507 | awk '{if($3>64000 && $3 <65537)count++}END{print count}'112

如果这些算排除了是我们系统本身的问题,那其他的系统呢,通过调查发现其他几个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的建议,在测试服务器的启动脚本里增加了:

export MALLOC_ARENA_MAX=4

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

#ifndef HEAP_MAX_SIZE# ifdef DEFAULT_MMAP_THRESHOLD_MAX#  define HEAP_MAX_SIZE (2 * DEFAULT_MMAP_THRESHOLD_MAX)# else#  define HEAP_MAX_SIZE (1024 * 1024) /* must be a power of two */# endif#endif

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,安装好后在启动脚本里加上

export LD_PRELOAD="/usr/lib64/libtcmalloc.so.4.1.0"

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

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

相关文章:

你感兴趣的文章:

标签云: