bigywater.tech
Java · 299

堆转储与 OQL:从对象图寻找泄漏引用

以前 jhat 自带 OQL,现在的 JDK 没有了。想「像查数据库一样查堆」,今天怎么做?这篇真的导出堆转储,用 Eclipse MAT 的批处理模式跑 OQL,再看不可达对象在转储里和在 MAT 里的区别。

34.07 MB
只含存活对象的堆转储大小(含不可达的是 40.76 MB;gzip 后 2.41 MB)
4000 × 4136
用 OQL 查出的过期 Session 数与每个的保留字节数
0 → 1
byte[5000000] 在 MAT 默认解析与 -keep_unreachable_objects 下的查询结果数

导出堆转储

实测 · 本机执行输出
  (另一个进程)HotSpotDiagnosticMXBean.dumpHeap(path, live=false): 40.47 MB ;dumpHeap(path, live=true): 34.09 MB
  jcmd <pid> GC.heap_dump -all f(先做,此时还没有 GC)                40.76 MB  耗时 0.21 s
  jmap -dump:format=b,file=f <pid>(不带 live,同样没有先 GC)         40.76 MB  耗时 0.09 s
  jcmd <pid> GC.heap_dump f(默认 live,会先做一次 GC)                34.07 MB  耗时 0.09 s
  jmap -dump:live,format=b,file=f <pid>                      34.07 MB  耗时 0.09 s
  jcmd <pid> GC.heap_dump -gz=1 f(压缩)                        2.41 MB  耗时 0.11 s
  jhat(曾经带 OQL 的堆分析器)是否存在于 JDK 的 bin 目录: {'JDK 17': False, 'JDK 21': False}

MAT 的 OQL

实测 · 本机执行输出
  年龄大于 65 的 User(字段读取与 toString())
    SELECT u.name.toString() AS name, u.age AS age FROM Heap$User u WHERE u.age > 65
    → 40 行,1.6 s;表头 name,age,;前 4 行 ['user249,67,', 'user248,66,', 'user499,67,', 'user498,66,']
  长度至少 500000 的 byte[]
    SELECT * FROM byte[] b WHERE b.@length >= 500000
    → 6 行,1.0 s;表头 Class Name,Shallow Heap,Retained Heap,;前 6 行 ['byte[600000] @ 0x11f800000,600016,600016,', 'byte[700000] @ 0x11f900000,700016,700016,', 'byte[800000] @ 0x11fa00000,800016,800016,', 'byte[900000] @ 0x11fb00000,900016,900016,', 'byte[1000000] @ 0x11fc00000,1000016,1000016,', 'byte[500000] @ 0x12e0806c0,500016,500016,']
  已过期的 Session 及其保留大小
    SELECT s.id, s.@retainedHeapSize FROM Heap$Session s WHERE s.expired = true
    → 4000 行,0.9 s;表头 s.id,s.@retainedHeapSize,;前 3 行 ['49,4136,', '48,4136,', '47,4136,']
    过期 Session 数 4000 ;保留大小合计 16544000 字节
  以 dup- 开头的 String
    SELECT toString(s) AS v, s.@retainedHeapSize AS retained FROM java.lang.String s WHERE toString(s).startsWith(\"dup-\")
    → 50002 行,2.1 s;表头 v,retained,;前 3 行 ['dup-5,48,', 'dup-4,48,', 'dup-3,48,']
    不同取值 12 种;个数为 5000 的有 10 种;其余取值: ['dup-\\u0001', 'dup-'] ;这些 String 的保留大小合计 2400096 字节(其中每个 String 平均 48.0 字节)

不可达对象

实测 · 本机执行输出
  dumpHeap(live=true) 默认解析                         长度 ≥ 5000000 的 byte[]: 0 个(查询没有结果) ;日志含 Removing unreachable objects: False
  dumpHeap(live=false) 默认解析                        长度 ≥ 5000000 的 byte[]: 0 个(查询没有结果) ;日志含 Removing unreachable objects: True
  dumpHeap(live=false) + -keep_unreachable_objects 长度 ≥ 5000000 的 byte[]: 1 个 ['byte[5000000] @ 0x120d00000,5000016,5000016,'] ;日志含 Removing unreachable objects: True

分步演示

堆转储与 OQL
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12 导出转储;Eclipse MAT 1.16.1(macOS aarch64,自带 JDK 17 运行);OQL 是 MAT 的方言,不是 JDK 内置功能
    • 没有验证:VisualVM 的 OQL(语法不同)、MAT 的 OQL 全部函数、Linux 上转储的差异、更大的堆下 ParseHeapDump 的内存与耗时、hprof 格式细节(只看了文件大小)、jhsdb、JFR 对象采样
    京ICP备2026067502号-1京公网安备11010502063487号