通过 core dump 转 Java heap dump

在诊断 Java 应用问题的时候, 经常需要做 heap dump. 因为 heap dump 就是一个资源宝库, 里面有各种运行时的内存数据, 结合源代码以及程序日志, 我们就能推断程序的运行状况, 发现问题的根源.

可是在我们获取 heap dump 的时候, 通常会面临下面这种窘境:

/usr/bin/jcmd 7674 GC.heap_dump /tmp/heap.log.hprof
7674:
com.sun.tools.attach.AttachNotSupportedException: Unable to open socket file: target process not responding or HotSpot VM not loaded
at sun.tools.attach.LinuxVirtualMachine.(LinuxVirtualMachine.java:106)
at sun.tools.attach.LinuxAttachProvider.attachVirtualMachine(LinuxAttachProvider.java:63)
at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:213)
at sun.tools.jcmd.JCmd.executeCommandForPid(JCmd.java:140)
at sun.tools.jcmd.JCmd.main(JCmd.java:129)

无论你是使用 jmap, 还是使用 jcmd, 几乎都是一样的结果: 无法 attach 到目标进程.

通常是由于目标 java 进程现在很忙, 比如全部 CPU 都在执行业务逻辑或者GC, 导致无法 attach 上去. 但是这个时候又是出问题的状态下, 需要及时获得有用的 heap dump. 偶尔有时候多试用几次会有用, 有时候却完全没用.

这种情况下, 如何获取 heap dump 呢?

答案是: 我们可以通过 core dump 转 heap dump.

虽然当前的 Java 进程因为繁忙不响应, 但是我们可以通过操作系统的指令, 让它停下来, 然后做一个core dump

什么是 core dump

core dump 是应用程序进程的内存状态的二进制映像, 它把应用程序在内存的状态全部保存到 dump 文件里, 这样后续就可以通过工具来查看当时程序的内存里内容.

如何获取 core dump

很多方式可以获取 core dump, 这里我们使用 gcore.

安装 core

gdb 包含 gcore 命令, 我们通过安装 gdb 就安装了 gcore.

$ sudo apt install gdb -y

core dump 文件相关设置

core dump 文件非常有用, 但是它通常非常大, 比如几个G, 默认情况下很多程序崩溃后或者收到某些signal 之后都会出发生成 core dump, 所以如果连续几次触发, 就会占用很多磁盘空间. 另外 core dump 是内存的映像, 所以会有很多敏感信息. 所以默认情况下, 操作系统通过设置允许最大 core dump 的大小来限制产生 core dump.

比如 Ubuntu 22.04 里面设置的 soft 大小都是0:

$ cat /etc/security/limits.conf
*               soft    core            0
#root            hard    core            100000

所以我们要改掉这个最大值, 以便我们能产生 core dump:

$ sudo cp /etc/security/limits.conf /etc/security/limits.d/   # 复制limits.conf模板文件到配置文件夹
$ sudo vim /etc/security/limits.d/limits.conf     # 编辑配置文件, 修改我们目标 Java 进程的用户的配置
#### 我们假设我们的目标进程使用 appUser 运行, 给他设置 core file 的 hard, soft 允许大小
         appUser hard core unlimited
         appUser soft core unlimited

$ sudo su appUser  # 切换到 appUser
$ ulimit -a        # 查看它的 core file size 的 hard, soft 大小   
### 有时候, 即便你改好了, 还是看到 core size 是0, 不过不要悲观, 继续下去, 可以参数 core dump.                                                                         
$ exit             # 退出 appUser 

获取 core dump

有了上面的设置之后, 我们就可以通过 gcore 产生 core dump 文件了.

$ pgrep java   # 显示查看目标进程的 pid
$ sudo gcore      # 使用 gcore 获取 core dump, 根据目标进程的内存占用大小, 可能会花费不同的时间
### 有时候使用别的用户, 即使使用 sudo 也不能 ptrace, 会得到 ptrace 无法 attach 的错误, 这时候, 切换到
### appUser 用户, 直接 gcore  就行. 如果报无法写入, 看你在哪个文件夹, 到一个可写的文件夹就好了
$ ls -lah core.   # 查看获取的 core dump 的大小, 通常在当前目录

core dump 转 heap dump

有了 core dump 之后, 我们需要通过 JDK 自带的工具把 core dump 转成 heap dump.

Hotspot JDK (Oracle 或者 OpenJDK)

对于 Hotspot 系列的 JDK

$ find /app -name jmap     # 找到运行目标进程的 JDK 里面的 jmap 命令
$ sudo /usr/bin/jmap  -dump:format=b,file=heap.hprof  /usr/bin/java ./core.   #转换

IBM JDK

$ find /app -name jextract  # 找到运行目标进程的 JDK 里面的 jextract 命令
$ jextract core.       # 从 core dump 转成 heap dump core..zip
## 对于这个 core..zip 使用 MAT 并且安装的 dtfj 插件, 可以打开, 或者使用 IBM JDK 的 jdmpview 工具里 
## 面的 heapdump 命令, 可以把 这个 zip 文件转换成 IBM PHD 格式的 heap dump, 然后使用带 DTFJ 插件的 MAT 
## 工具分析. 
$ /usr/bin/jdmpview 
$ heapdump
$ ls -lah
-rw-r--r--  1 root    root    7.5G Aug 21 17:51 core.16751
-rw-rw-r--  1 user1   user1   926M Aug 21 19:09 core.16751.zip
-rw-rw-r--  1 user1   user1    19M Aug 21 19:46 core.16751.zip.phd