在堆转储里找到元凶
目标
亲手制造 OutOfMemoryError,通过堆转储和类直方图找出元凶,并把防止复发的启动选项写进脚本留存。
为什么重要
转储只会在事件发生的那一刻生成。不开启选项,事件之后就什么都没有了。而且 OutOfMemoryError 并不会让进程死掉——ExitOnOutOfMemoryError 会终结那种半死不活、却能通过健康检查的状态。材料 Leak.java 会不断把 -Dleak.kb=256 大小的数组放入静态映射(如果 -Dleak.retain=false 则不放入),用 -Dleak.sleep.ms 决定速度,用 -Dleak.iterations 决定次数。开始时会打印 max_heap_mb=。
步骤
- 在
/root/jvm/heap中编译 Leak.java,并用java -Xmx64m -cp /root/jvm/heap Leak > /root/jvm/heap/oom.log 2>&1运行。日志中必须有java.lang.OutOfMemoryError: Java heap space。 - 加上
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/root/jvm/heap/oom.hprof再运行一次,并把输出保存到/root/jvm/heap/oom-dump.log。必须生成oom.hprof(1MB 以上,以JAVA PROFILE 1.0.2开头),并且日志中有Heap dump file created。 - 在后台启动
java -Xmx64m -Dleak.sleep.ms=50 -cp /root/jvm/heap Leak,2 秒之后留下jcmd <pid> GC.class_histogram > /root/jvm/heap/histo.txt和jcmd <pid> GC.heap_dump /root/jvm/heap/live.hprof(之后可以 kill 掉该进程)。 - 从 histo.txt 的第 1 名那一行读出类名和实例数,以
top_class=<이름>(占位符为名称)和instances=<n>两行写入/root/jvm/heap/top.txt。 - 用
java -Xmx64m -Dleak.retain=false -Dleak.iterations=3000 -cp /root/jvm/heap Leak > /root/jvm/heap/noleak.log 2>&1运行相同的分配,但不持有引用。日志中必须有done iterations=3000,且不能有 OutOfMemoryError。 - 加上
-XX:+ExitOnOutOfMemoryError运行,把输出保存到/root/jvm/heap/exit-on-oom.log,把退出码(echo $?)保存到/root/jvm/heap/exit.txt。日志中必须有Terminating due to java.lang.OutOfMemoryError,且 exit.txt 必须是 3。 - 读取
java -XX:+PrintFlagsFinal -version中的MaxHeapSize值,以及再加上-XX:MaxRAMPercentage=50之后的值,以default_max_heap=<바이트>和half_max_heap=<바이트>(占位符均为字节数)两行写入/root/jvm/heap/ram.txt。 - 创建
/root/jvm/heap/start.sh:这个脚本用-Xmx64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/root/jvm/heap/dumps -XX:+ExitOnOutOfMemoryError -Xlog:gc:file=/root/jvm/heap/gc.log运行 Leak,并把退出码写入/root/jvm/heap/runbook-exit.txt。运行之后,/root/jvm/heap/dumps/中必须生成 .hprof,必须生成 gc.log,并且 runbook-exit.txt 必须是 3。
参考
- 确认 HPROF 文件头:
dd if=/root/jvm/heap/oom.hprof bs=1 count=18 2>/dev/null→JAVA PROFILE 1.0.2 - 读取 MaxHeapSize:
java -XX:+PrintFlagsFinal -version 2>/dev/null | awk '/ MaxHeapSize /{print $4}' - 如果给 HeapDumpPath 指定的是目录,就会在其中生成 java_pid.hprof。请提前创建目录。
- 常见错误:把转储路径放在会消失的地方;没有 ExitOnOutOfMemoryError 而放任僵尸状态;只看直方图第 1 名([B)就收场。
制造 OutOfMemoryError
在 /root/jvm/heap 中编译 Leak.java,用 -Xmx64m 运行,并把输出保存到 /root/jvm/heap/oom.log。必须有 java.lang.OutOfMemoryError: Java heap space。
先执行 javac -d /root/jvm/heap /opt/lab/fixtures/jvm/Leak.java,再执行 java -Xmx64m -cp /root/jvm/heap Leak > oom.log 2>&1。几秒之内堆就会被占满。
在事件发生的那一刻获取转储
加上 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/root/jvm/heap/oom.hprof 运行,并把输出保存到 /root/jvm/heap/oom-dump.log。必须有 oom.hprof(1MB 以上,以 JAVA PROFILE 1.0.2 开头)和 Heap dump file created。
这两个选项要放在类名之前。转储是在异常被抛出的瞬间由 JVM 生成的——日志中会打印 Dumping heap to ...。
运行中进程的直方图和转储
用 -Xmx64m -Dleak.sleep.ms=50 在后台启动 Leak,2 秒之后留下 jcmd GC.class_histogram > /root/jvm/heap/histo.txt 和 jcmd GC.heap_dump /root/jvm/heap/live.hprof。
先执行 java ... & PID=$!,然后 sleep 2,再调用这两个 jcmd。heap_dump 会先请求一次 Full GC,所以只会留下可达的对象。
读懂直方图的第一行
从 histo.txt 的第 1 名那一行读出类名和实例数,以 top_class=<名称> 和 instances= 的形式写入 /root/jvm/heap/top.txt。
第 1 名那一行以 ' 1:' 开头,各列依次是排名、实例数、字节数、类名。名称取到 (java.base@…) 之前。awk '$1=="1:"{print $2, $4}' 可以给出这两个值。
同样的分配,只是没有引用
用 -Xmx64m -Dleak.retain=false -Dleak.iterations=3000 运行,并把输出保存到 /root/jvm/heap/noleak.log。必须有 done iterations=3000,且不能有 OutOfMemoryError。
虽然分配了 3000 × 256KB = 750MB,但在 64MB 的堆上也能一直运行到结束。因为一放手,GC 就会回收。
不要制造僵尸
加上 -XX:+ExitOnOutOfMemoryError 运行,把输出保存到 /root/jvm/heap/exit-on-oom.log,把退出码保存到 /root/jvm/heap/exit.txt。必须有 Terminating due to java.lang.OutOfMemoryError,且 exit.txt 必须是 3。
java ... > exit-on-oom.log 2>&1; echo $? > exit.txt。JVM 会在第一次 OutOfMemoryError 时立即结束,退出码为 3(实测)。
测量容器堆的默认值
把 java -XX:+PrintFlagsFinal -version 的 MaxHeapSize,以及加上 -XX:MaxRAMPercentage=50 之后的 MaxHeapSize,以 default_max_heap=<字节数> 和 half_max_heap=<字节数> 的形式写入 /root/jvm/heap/ram.txt。
用参考部分的 awk 提取值,第二次是把 -XX:MaxRAMPercentage=50 放在 -XX:+PrintFlagsFinal 之前。默认是 25%,所以第二个值大约是第一个的两倍。
把选项固定在脚本里
创建 /root/jvm/heap/start.sh:用 -Xmx64m、HeapDumpOnOutOfMemoryError、HeapDumpPath=/root/jvm/heap/dumps、ExitOnOutOfMemoryError、-Xlog:gc:file=/root/jvm/heap/gc.log 运行 Leak,并把退出码写入 /root/jvm/heap/runbook-exit.txt。运行之后,dumps/ 中会生成 .hprof、gc.log 和 runbook-exit.txt(3)。
如果给 HeapDumpPath 指定的是目录,就会生成 java_pid.hprof——在脚本第一行加上 mkdir -p。最后一行是 echo $? > runbook-exit.txt。