java 堆外内存泄露定位

现象

  1. 服务器运行一段时间后,内存占用越来越高,最终导致服务器 OOM。
  2. 通过查看相关的快照和监控,发现堆内存使用正常,但是机器的 RSS 一直在增长。


预排查

  1. 是否创建了大对象
  2. 是否使用了堆外内存,且未及时释放内存
  3. 是使用了 jni 方法,且未及时释放内存
  4. 是否使用了相关资源, 比如 stream, file, socket 等,且未及时释放资源

定位

  1. 首先使用 jmap 把内存快照 dump 下来


如果是堆内内存泄露的话, 通过快照分析基本就能定位到问题,但是堆外内存泄露的话,就需要通过其他方式定位了

  1. 同时通过确认线程状态和数量,确认也不是线程数量导致的内存泄露
  2. 因此需要排查是否是堆外内存泄露导致的
0. 开启 NMT

由于是线上环境,暂不考虑开启 NMT,因此需要通过其他方式定位

1. 使用 jmap 命令

发现 MaxMetaspaceSize 未设置最大值,那么是否可能是元空间内存泄露导致的呢?

2. 观察类加载和卸载情况

在本地环境,添加启动参数 -verbose:class,观察类加载和卸载情况
再本地环境跑一段时间,然后手动出发 gc, 通过 jmap 命令或者 System.gc()
发现类加载也正常, 没有什么特别的情况

3. 使用 strace 和 pmap 命令

参考 https://ww.dandelioncloud.cn/article/details/1599739989472788481

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pmap -x PID
````

![img_2.png](img_2.png)

发现确实有大内存的使用

使用 strace 命令追踪系统调用

```shell
## 追踪PID进程的brk,mmap,munmap系统调用
## 查看那些线程申请了较大内存, 然后在jstack查看线程栈信息,注意线程号 10进制和16进制的转换
strace -f -e "brk,mmap,munmap" -p PID
## 输出线程栈信息, 查看那个线程申请了大内存,然后具体看代码排查
jstack -l PID > file.dump
3. 使用 gperftools
linux
1
2
3
4
5
6
7
8
9
10
## 使用 yum 安装
yum install gperftools libunwind
## 可选,PDF 支持
yum install graphviz
## 设置环境变量, 可在idea启动设置中添加,地址应该不一样,具体看安装路径
export LD_PRELOAD=/usr/lib64/libtcmalloc.so
export HEAPPROFILE=/DATA1/admin_tmp/gzip
## 启动 Java 程序
## 分析内存占用,文本显示结果
pprof --text /usr/bin/java /DATA1/admin_tmp/gzip.0001.heap
mac
1
2
3
4
5
6
7
8
9
10
## 使用 Brew 安装
brew install gperftools 或者 brew install --build-from-source gperftools
## 可选,PDF 支持
brew install graphviz 或者 brew install --build-from-source graphviz
## 设置环境变量, 可在idea启动设置中添加,地址应该不一样,具体看安装路径
export DYLD_INSERT_LIBRARIES=/usr/local/Cellar/gperftools/2.8.1/lib/libtcmalloc.dylib
export HEAPPROFILE=/Users/cheng/tmp/test.log
## 启动 Java 程序
## 分析内存占用,文本显示结果
pprof --text /Library/Java/JavaVirtualMachines/jdk-11.0.10.jdk/Contents/Home/bin/java ~/tmp *.heap