最近开发同学反馈,某定时任务服务疑似有内存泄漏,整个进程的内存占用比 Xmx 内存大不少,而且看起来是缓慢上升的,做了下面这次分析,包括下面的内容:

  • 分析 JVM native 内存的一些常见思路
  • 内存增长了,怎么甄别是不是内存泄漏
  • 一个完全不熟悉的项目如何找到可能导致 native 内存分配的代码
  • 经典的 Linux 64M 内存问题
  • 到底是内存碎片还是内存泄漏

现象

这个定时任务的应用设置 Xmx 为 925M,但是 native 内存缓存持续增长,但是增长到一定阶段也会保持稳定,不再继续增长。

是内存泄漏吗?

不管是不是内存泄漏,首先要搞清楚的是这段增长的内存是什么,土方法就是用 pmap -x 持续观察内存地址空间的变化。

经过几个小时的 pmap 后台运行,很快发现堆内存几乎无变化,增长的区域都在 64M 内存空间,这就是经典的 glibc 内存分配 64M 问题。

关于 Linux 64M 内存问题,我之前写过几篇相关的文章,大家感兴趣可以去看。

从这里基本可以确定是 native 带来的问题,接下来就是 dump 出来看里面到底存了什么。这里有几个方法

  • 使用 gdb
  • 写一个脚本读取 /proc/<pid>/mem
  • 我自己用 Go 写的一个小工具(可能过段时间释放出来)

脚本内容如下:

cat /proc/$1/maps | grep -Fv ".so" | grep " 0 " | awk '{print $1}' | grep $2 | ( IFS="-"
while read a b; do
dd if=/proc/$1/mem bs=$( getconf PAGESIZE ) iflag=skip_bytes,count_bytes \
skip=$(( 0x$a )) count=$(( 0x$b - 0x$a )) of="$1_mem_$a.bin"
done )
复制代码

执行这个脚本,传入进程号和起始地址就可以把对应内存 dump 到文件中。接下来可以通过 strings 初步查看文件里面有没有认识的字符串。通过 strings 发现很多 jar 包文件里的内容,部分内容如下:

这个内容是项目依赖 jar 包 HikariCP-2.5.1.jar 的 MANIFEST.MF 文件的内容

.
├── MANIFEST.MF
└── maven└── com.zaxxer└── HikariCP├── pom.properties└── pom.xml
复制代码

看来就是程序就是读了 HikariCP-2.5.1.jar 的内容,通过 16 进制分析可以进一步确认。众所周知 jar 包就是一个 zip,如果读取了 zip,那理论内存中会有 zip 的魔数,问一下 ChatGPT zip 的魔数是多少。

用 010 Editor 拿着 50 4B 03 04 去内存里搜,可以看到这个 1M 多的内存文件里有 15 个 zip 魔数。

可以进一步把这个文件当做 zip 文件来解析,可以看到 zip 文件对应的 zip entry 有哪些。

接下来就是去找是谁在读这些 jar 包,读文件会有系统调用,于是这里 strace 就可以看看到底是怎么读的。(也可以通过 jstack 看 java 层的堆栈找到同样的原因,这里不展开)

这里出现了一个不认识的临时文件,还有一个前缀 FastClasspathScanner,去代码里搜,原理是项目用了 FastClasspathScanner 来扫描 class 文件

FastClasspathScanner 项目地址在 github.com/classgraph/… ,FastClasspathScanner 提供了一种简单快速的方法来扫描 Java 类路径。它可以轻松找到类路径上的所有类、资源、包和模块,并获取有关它们的信息。这个项目用它来做什么呢?

经过看代码,它大概是用来去 jar 包里搜哪些类实现了
com.seewo.school.statistics.counter.Counter 接口,然后去 classpath 中的找到实现了这个接口的类,也就是遍历所有的 jar 包去找实现类。

FastClasspathScanner 的做法是先把这些依赖的 jar 包先拷贝到临时目录(注意这里的 tempFile.deleteOnExit(),虽然跟此次问题不相关,但也是一个内存隐患,等下介绍)

然后读取这些临时 jar 包,

大量申请释放内存的地方在 java.util.zip.Inflater 类,调用它的 end 方法会释放 native 的内存。如果 end 方法没有调用,就会导致内存泄漏,
java.util.zip.InflaterInputStream 类的 close 方法在一些场景下是不会调用 Inflater.end 方法,如下所示。

但是 Inflater 类有实现 finalize 方法,在 Inflater 对象不可达以后,JVM 会帮忙调用 Inflater 类的 finalize 方法

public class Inflater {public void end() {synchronized (zsRef) {long addr = zsRef.address();zsRef.clear();if (addr != 0) {end(addr);buf = null;}}}protected void finalize() {end();}private native static void initIDs();// ...private native static void end(long addr);
}
复制代码

有几种可能性

  • Inflater 因为被其它对象引用,没能释放,导致 finalize 方法不能被调用,内存自然没法释放
  • Inflater 因为还没被 FinalizerThread 执行 fianlize 方法,导致没有释放
  • Inflater 的 finalize 方法被调用,但是被 libc 的 ptmalloc 缓存,没能真正释放回操作系统

更多关于 finalize 机制,大家可以移步笨神的文章:「JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版) 」 heapdump.cn/article/265…

于是 dump 堆内存去分析是不是有大量的 Inflater 类没有被回收,经过内存分析看,发现 java.util.zip.Inflater 类有 6k 多没有被回收。

没有被回收的原因是它们被 Finalizer 引用,需要两次 GC 才有可能被回收。

而且 FinalizerThread 的优先级比较低,如果 CPU 比较紧张的情况下,会导致需要很久才会把队列中 f 对象的 finalize 方法执行完。又因为这个时间比较长,可能导致 f 对象多次 GC 以后进到老年代,如果老年代 gc 频率不高,那 f 对象存活的时间就更久了。

这样的 native 内存短时间不释放,又由于定时任务长期执行,就可能会导致内存碎片、glibc 内存不归还的出现(等下验证),就算释放 libc 也有可能不会还给操作系统。

通过手动多次触发 GC,确认可以将所有的 java.util.zip.Inflater 回收掉,但是 natvie 内存并没有太大的变化。于是怀疑是 glibc 的内存碎片和内存没有归还给操作系统。

如何修改

有几种可能的修改方式

方案 1:其实这里明显是程序上设计不合理,没必要每次定时任务都去扫描包,这些包又不会变,扫描一次就可以了,让开发的同学去修改代码,把第一次扫描的结果缓存起来。然后打了一个包去开发环境运行,效果非常明显,新版本跑了一整天都内存几乎没有什么波动,旧版本则缓慢的上涨了 400M 左右。

方案 2:修改 FastClasspathScanner 代码,在流关闭的时候,顺带关闭 Inflater, SpringBoot 里面是这么实现的。(不想改了)

SpringBoot 里面的改动如下:github.com/spring-proj…

方案 3:前面怀疑是因为 glibc 的内存碎片,尝试替换碎片整理更友好的 tcmalloc 或者 jemalloc,看看效果。

LD_PRELOAD=/usr/local/lib/libtcmalloc.so java -jar xxx
复制代码

下面是换了 tcmalloc 以后的效果,tcmalloc 贼稳。

可以看到换到了对内存碎片更友好的内存分配器以后,内存的增长得到了非常好的控制。

番外篇

上面提到 tempFile.deleteOnExit() 会有巨大的坑,通过内存 dump 的分析,可以看到 java.io.DeleteOnExitHook 占了将近 40M。

里面有一个静态的 hashset,里面存了 10 几万个字符串,就是 FastClasspathScanner 产生的临时文件路径。

是因为这里调用了 File.deleteOnExit,这个可太坑了。

它把文件的路径加到了一个 jvm 全局 DeleteOnExitHook 类的静态变量 files 中。

又因为临时文件每次的路径都是不一样的,导致这个 hashset 随着定时任务的执行逐渐变大,永远无法回收。

DeleteOnExitHook 本意是用来在 Java 虚拟机退出的时候删除文件。

对于 server 端这种长时间运行的程序,用 deleteOnExit 就太坑了,只有等容器退出那会才会执行删除。再加上这里的文件路径每次都变,导致内存白白浪费。

小结

因为程序设计的问题导致频繁读取 jar 包(实际是 zip 文件),需要调用 native 的代码去处理 zip 文件,会有非常多 native 内存分配的产生。又因为用了 zip 默认的 InflaterInputStream,导致没有办法在流关闭时调用 java.util.zip.Inflater 类的 end 方法释放 native 内存,只能等到 Finalizer 机制在多次 GC 以后调用,导致了 native 内存可能在短时间内无法释放。

又因为内存碎片和 libc 内存分配器的实现策略,导致了它没有将内存真正释放给操作系统,导致了缓慢的内存增长。

简单来说,有一个猪队友在不停的申请内存(无法立刻释放),又由于 libc 碎片化和内存二道贩子不一定会把 native 内存还给 os,导致了内存的缓慢增长。

一点想法:

  • Java 的 zip 机制是真的设计有点坑,
  • Finalize 机制完全帮倒忙,弊远大于利,新版本 Java 确实也做了修改。

一次疑似 JVM native 内存泄漏的排查实录相关推荐

  1. 我们有一个线上的项目,刚启动完就占用了超过 1.5G,一次大量 JVM Native 内存泄露的排查分析(64M 问题)

    我们有一个线上的项目,刚启动完就占用了使用 top 命令查看 RES 占用了超过 1.5G,这明显不合理,于是进行了一些分析找到了根本的原因,下面是完整的分析过程,希望对你有所帮助. 会涉及到下面这些 ...

  2. 一次完整的JVM堆外内存泄漏故障排查记录

    前言 记录一次线上JVM堆外内存泄漏问题的排查过程与思路,其中夹带一些JVM内存分配机制以及常用的JVM问题排查指令和工具分享,希望对大家有所帮助. 在整个排查过程中,我也走了不少弯路,但是在文章中我 ...

  3. Android Native 内存泄漏系统化解决方案

    导读:C++内存泄漏问题的分析.定位一直是Android平台上困扰开发人员的难题.因为地图渲染.导航等核心功能对性能要求很高,高德地图APP中存在大量的C++代码.解决这个问题对于产品质量尤为重要和关 ...

  4. 联合国数据库疑似被中国黑客泄漏

    图片说明是国内知名黑客"越南邻国宰相"提交的. 下面是他们微博发出的信息证明 可以看出这些图,他们确实入侵了联合国的网站. 以上由163微论坛最新报道 联合国数据库疑似被中国黑客泄 ...

  5. Android Native内存泄漏诊断

    Android Native内存泄漏诊断 1.基础诊断方法 特点:操作简单,但只能判断是否有泄漏,但需使用者自行判断泄漏在哪里 命令行方式 adb shell dumpsys meminfo vStu ...

  6. Java内存泄漏的排查

    1.内存溢出 一种通俗的说法. 1.内存溢出:你申请了10个字节的空间,但是你在这个空间写入11或以上字节的数据,出现溢出. 2.内存泄漏:你用new申请了一块内存,后来很长时间都不再使用了(按理应该 ...

  7. python 内存泄漏的排查

    python 内存泄漏的排查 判断该次上线或发版的内容,排查到具体上线了那些接口或修改了那些接口 单起一个服务,使用ps -aux |grep pid 查看该进程占用的内存大小 work@xxx:~$ ...

  8. 【JVM】内存溢出问题排查

    一.问题背景 下午突发服务器CPU频繁撑爆,服务启动后不久就挂掉.一周前系统有一次投产,之后再没有更新过系统.同时在日志中看到大量的dubbo服务调用失败. 二.排查问题产生原因 1.查看JVM崩溃日 ...

  9. 一次 Java 内存泄漏的排查

    由来 前些日子小组内安排值班,轮流看顾我们的服务,主要做一些报警邮件处理.Bug 排查.运营 issue 处理的事.工作日还好,无论干什么都要上班的,若是轮到周末,那这一天算是毁了. 不知道是公司网络 ...

最新文章

  1. ItemAdding事件接收器中无法取到【创建者】的字段的值
  2. 高小明的云平台搭建系列之一——物理机装 ESXi 5.0
  3. anaconda与pip 清华镜像源
  4. 网易云信携手武汉三好教育,共筑教育援疆的「云桥梁」
  5. 浙企加入中国大数据产业生态联盟 共商数据价值
  6. sudo apt-get常用命令
  7. CMake笔记-使用CMake GUI生成MinGW的Makefiles及编译hiredis
  8. 在同一个公司死磕5-10年,到底值不值得?
  9. ENVI入门系列教程---一、数据预处理---4.3自定义RPC文件图像正射校正
  10. 内部排序算法(Golang版本)
  11. 微信小程序后端Java接口开发
  12. AD的PCB文件无法保存问题
  13. 农作物病虫害识别进展概述(***)
  14. VMware Workstation(vm虚拟机)
  15. 《Kotin 极简教程》第9章 轻量级线程:协程(2)
  16. windows10下使用Ubuntu子系统
  17. 近半数受访企业年度调薪比例在5%以下,约40%企业年度调薪率与上年度相比保持不变 | 美通社头条...
  18. python+uiautomation,怎么学习,雪地跪求大佬赐教
  19. 【MySql】解决安装时,无自定义安装
  20. 【转载】Camera安卓源码-高通mm_camera架构剖析

热门文章

  1. 【CTF】 2018_rop
  2. 致敬马克龙访华?法国品牌手机接入鸿蒙!?---转自百度新闻
  3. 一年读50本书还是读10本书收获更大?
  4. 【考前冲刺】计算机三级网络技术之综合题-sniffer抓包分析
  5. educoder基本SR锁存器+门控SR锁存器+与非门构成的门控SR锁存器
  6. 使用JMeter作为MQTT客户端
  7. 使用Docker安装OSX
  8. ifeq makefile 或语句_Makefile ifeq、ifneq、ifdef和ifndef(条件判断)
  9. php登陆成功会员页,dedeCMS设置会员登陆跳转页面
  10. QT5.14——模拟交通灯(一)