关于后端:Java应用堆外内存泄露问题排查-京东云技术团队

7次阅读

共计 2851 个字符,预计需要花费 8 分钟才能阅读完成。

问题是怎么发现的

最近有个 java 利用在做压力测试
压测环境配置:
CentOS 零碎 4 核 CPU 8g 内存 jdk1.6.0_25,jvm 配置 -server -Xms2048m -Xmx2048m
呈现问题如下
执行 300 并发,压测继续 1 个小时后内存使用率从 20% 回升到 100%,tps 从 1100 多升高到 600 多。

排查问题的具体过程

首先应用 top 命令查看内存占用如下

而后查看 java 堆内存散布状况,查看堆内存占用失常,jvm 垃圾回收也没有异样。

而后想到了是堆外内存透露,因为零碎中用的 jsf 接口比拟多,底层都是依赖的 netty。

  • 首先思考的是 java 中 nio 包下的 DirectByteBuffer, 能够间接调配堆外内存,不过该类调配的内存也有大小限度的,能够间接通过 -XX:MaxDirectMemorySize=1g 进行指定,并且内存不够用的时候代码中会显式的调用 System.gc() 办法来触发 FullGC, 如果内存还是不够用就会抛出内存溢出的异样。
  • 为了验证这一想法,于是在启动参数中通过 -XX:MaxDirectMemorySize=1g 指定了堆外内存大小为 1g,而后再次进行压测,发现内存还是在持续增长,而后超过了堆内存 2g 和堆外内存 1g 的总和,并且也没有发现有内存溢出的异样,也没有频繁的进行 FullGC。所以可能不是 nio 的 DirectByteBuffer 占用的堆外内存。

为了剖析堆外内存到底是谁占用了,不得不装置 google-perftools 工具进行剖析。它的原理是在 java 利用程序运行时,当调用 malloc 时换用它的 libtcmalloc.so,这样就能做一些统计了。
装置步骤如下:

  • 下载 http://download.savannah.gnu.org/releases/libunwind/libunwind…,
  • ./configure
  • make
  • sudo make install // 须要 root 权限
  • 下载 http://google-perftools.googlecode.com/files/google-perftools…,
  • ./configure –prefix=/home/admin/tools/perftools –enable-frame-pointers
  • make
  • sudo make install // 须要 root 权限
  • 批改 lc\_config: sudo vi /etc/ld.so.conf.d/usr-local\_lib.conf,退出 /usr/local/lib(libunwind 的 lib 所在目录)
  • 执行 sudo /sbin/ldconfig,使 libunwind 失效
  • 在应用程序启动前退出:
  • export LD_PRELOAD=/home/admin/tools/perftools/lib/libtcmalloc.so
  • export HEAPPROFILE=/home/admin/heap/gzip
  • 启动应用程序,此时会在 /home/admin/heap 下看到诸如 gzip_pid.xxxx.heap 的 heap 文件
  • 应用 /home/admin/tools/perftools/bin/pprof –text $JAVA\_HOME/bin/java test\_pid.xxxx.heap 来查看
  • /home/admin/tools/perftools/bin/pprof –text $JAVA\_HOME/bin/java gzip\_22366.0005.heap > gzip-0005.txt
  • 而后查看剖析后果如下
Total: 4504.5 MB
4413.9 98.0% 98.0% 4413.9 98.0% zcalloc
60.0 1.3% 99.3% 60.0 1.3% os::malloc
16.4 0.4% 99.7% 16.4 0.4% ObjectSynchronizer::omAlloc
8.7 0.2% 99.9% 4422.7 98.2% Java_java_util_zip_Inflater_init
4.7 0.1% 100.0% 4.7 0.1% init
0.3 0.0% 100.0% 0.3 0.0% readCEN
0.2 0.0% 100.0% 0.2 0.0% instanceKlass::add_dependent_nmethod
0.1 0.0% 100.0% 0.1 0.0% _dl_allocate_tls
0.0 0.0% 100.0% 0.0 0.0% pthread_cond_wait@GLIBC_2.2.5
0.0 0.0% 100.0% 1.7 0.0% Thread::Thread
0.0 0.0% 100.0% 0.0 0.0% _dl_new_object
0.0 0.0% 100.0% 0.0 0.0% pthread_cond_timedwait@GLIBC_2.2.5
0.0 0.0% 100.0% 0.0 0.0% _dlerror_run
0.0 0.0% 100.0% 0.0 0.0% allocZip
0.0 0.0% 100.0% 0.0 0.0% __strdup
0.0 0.0% 100.0% 0.0 0.0% _nl_intern_locale_data
0.0 0.0% 100.0% 0.0 0.0% addMetaName

能够看到是 Java\_java\_util\_zip\_Inflater_init 这个函数始终在进行内存调配,查看 java 源码原来是

public GZIPInputStream(InputStream in, int size) throws IOException {super(in, new Inflater(true), size);
 usesDefaultInflater = true;
 readHeader(in);
}

 原来是 java 中 gzip 解压缩类耗尽了零碎内存,而后跟踪源码到了零碎里边应用的 jimdb 客户端 SerializationUtils 类,jimdb 客户端应用该工具类对保留在 jimdb 中的 key 和对象进行序列化和反序列化操作,并且在对 Object 类型的进行序列化和反序列化的时候用到了 gzip 解压缩, 也就是在调用 jimdb 客户端的 getObject 和 setObject 办法时,外部会应用 java 的 GZIPInputStream 和 GZIPOutputStream 解压缩性能,当大并发进行压测的时候,就会造成内存透露,呈现内存持续增长的问题,当压测进行后,内存也不会开释。

如何解决问题

1、降级 jdk 版本为 jdk7u71,压测一段时间后,发现内存增长有所减慢,并且会稳固在肯定的范畴内,不会把服务器的所有内存耗尽。猜想可能是 jdk1.6 版本的 bug
2、尽量不要应用 jimdb 客户端的 getObject 和 setObject 办法,如果真的须要保留对象,能够本人实现序列化和反序列化,不要解压缩性能,因为对象原本就不大,压缩不了多少空间。如真的须要解压缩性能,最好设置解压缩阀值,当对象大小超过阀值之后在进行解压缩解决,不要将所有对象都进行解压缩解决。

作者:京东批发 曹志飞

起源:京东云开发者社区

正文完
 0