欢迎您光临本小站。希望您在这里可以找到自己想要的信息。。。

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

java water 6768℃ 0评论

我们的一个系统上线后发现内存占用非常高,已分配内存达到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的博客Lex Chou的博客Live free or die原文地址:Java 进程在64位linux下占用巨大内存的分析, 感谢原作者分享。

转载请注明:学时网 » Java 进程在64位linux下占用巨大内存的分析

喜欢 (2)or分享 (0)

您必须 登录 才能发表评论!