我们的一个系统上线后发现内存占用非常高,已分配内存达到11G,而已分配地址空间更是17G了,而根据jmap执行结果发现:
Attaching to process ID 1507, please wait... Debugger attached successfully. Server compiler detected. JVM version is 24.0-b56 using 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 Generation Eden Space: capacity = 957349888 (913.0MB) used = 861426704 (821.5205230712891MB) free = 95923184 (91.47947692871094MB) .9803420669539% used From Space: capacity = 326107136 (311.0MB) used = 266214288 (253.88172912597656MB) free = 59892848 (57.11827087402344MB) .63399650352945% used To Space: capacity = 326631424 (311.5MB) used = 0 (0.0MB) free = 326631424 (311.5MB) .0% used PS Old Generation capacity = 689438720 (657.5MB) used = 395480112 (377.1592254638672MB) free = 293958608 (280.3407745361328MB) .36261984241326% used PS Perm Generation capacity = 134217728 (128.0MB) used = 55542368 (52.969329833984375MB) free = 78675360 (75.03067016601562MB) .38228893280029% used interned Strings occupying 2169000 bytes.
实际Java程序只用了1.4G内存,Xmx配置的是4096M,那么理论上应该只有4G多一点的RSS,继续使用pmaps分析:
fb870000000 65488 25100 25100 rw--- [ anon ] fb873ff4000 48 0 0 ----- [ anon ] fb874000000 65508 22560 22560 rw--- [ anon ] fb877ff9000 28 0 0 ----- [ anon ] fb878000000 65488 22772 22772 rw--- [ anon ] fb87bff4000 48 0 0 ----- [ anon ] fb87c000000 65496 62200 62200 rw--- [ anon ] fb87fff6000 40 0 0 ----- [ anon ] fb880000000 65516 65516 65516 rw--- [ anon ] fb883ffb000 20 0 0 ----- [ anon ] fb884000000 65488 65488 65488 rw--- [ anon ] fb887ff4000 48 0 0 ----- [ anon ] fb888000000 65492 65492 65492 rw--- [ anon ] fb88bff5000 44 0 0 ----- [ anon ] fb88c000000 65500 65500 65500 rw--- [ anon ] fb88fff7000 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}'
如果这些算排除了是我们系统本身的问题,那其他的系统呢,通过调查发现其他几个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看问题出在哪里。
点赞作者:Lex Chou的博客 Live free or die原文地址:Java 进程在64位linux下占用巨大内存的分析, 感谢原作者分享。
转载请注明:学时网 » Java 进程在64位linux下占用巨大内存的分析