写点什么

Java 应用堆外内存泄露问题排查 | 京东云技术团队

  • 2023-08-17
    北京
  • 本文字数:1968 字

    阅读完需:约 6 分钟

Java应用堆外内存泄露问题排查 | 京东云技术团队

问题是怎么发现的

最近有个 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-0.99-beta.tar.gz,

  • ./configure

  • make

  • sudo make install //需要 root 权限

  • 下载 http://google-perftools.googlecode.com/files/google-perftools-1.8.1.tar.gz,

  • ./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 MB4413.9 98.0% 98.0% 4413.9 98.0% zcalloc60.0 1.3% 99.3% 60.0 1.3% os::malloc16.4 0.4% 99.7% 16.4 0.4% ObjectSynchronizer::omAlloc8.7 0.2% 99.9% 4422.7 98.2% Java_java_util_zip_Inflater_init4.7 0.1% 100.0% 4.7 0.1% init0.3 0.0% 100.0% 0.3 0.0% readCEN0.2 0.0% 100.0% 0.2 0.0% instanceKlass::add_dependent_nmethod0.1 0.0% 100.0% 0.1 0.0% _dl_allocate_tls0.0 0.0% 100.0% 0.0 0.0% pthread_cond_wait@GLIBC_2.2.50.0 0.0% 100.0% 1.7 0.0% Thread::Thread0.0 0.0% 100.0% 0.0 0.0% _dl_new_object0.0 0.0% 100.0% 0.0 0.0% pthread_cond_timedwait@GLIBC_2.2.50.0 0.0% 100.0% 0.0 0.0% _dlerror_run0.0 0.0% 100.0% 0.0 0.0% allocZip0.0 0.0% 100.0% 0.0 0.0% __strdup0.0 0.0% 100.0% 0.0 0.0% _nl_intern_locale_data0.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 方法,如果真的需要保存对象,可以自己实现序列化和反序列化,不要解压缩功能,因为对象本来就不大,压缩不了多少空间。如真的需要解压缩功能,最好设置解压缩阀值,当对象大小超过阀值之后在进行解压缩处理,不要将所有对象都进行解压缩处理。


作者:京东零售 曹志飞

来源:京东云开发者社区

发布于: 刚刚阅读数: 4
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
Java应用堆外内存泄露问题排查 | 京东云技术团队_Java_京东科技开发者_InfoQ写作社区