TT Lab
はじめる
学ぶ 学習パス コース

ヒープは余っていたのにサービスが止まった

ヒープダンプで犯人を探す

TT Labで続きを見る

目標

OutOfMemoryErrorを自分で再現し、ヒープダンプとクラスヒストグラムで犯人を探し、再発を防ぐ起動オプションをスクリプトとして残します。

なぜ重要なのか

ダンプは事件の瞬間にしか作られません。オプションを有効にしていなければ、事件のあとには何も残りません。そしてOutOfMemoryErrorはプロセスを終了させません。半分死んだままヘルスチェックを通る状態を、ExitOnOutOfMemoryErrorが終わらせます。用意されたLeak.javaは、-Dleak.kb=256のサイズの配列を静的なマップに入れ続け(-Dleak.retain=falseなら入れません)、-Dleak.sleep.msで速度を、-Dleak.iterationsで回数を決めます。開始時にmax_heap_mb=を出力します。

ステップ

  1. /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が含まれている必要があります。
  2. -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が含まれている必要があります。
  3. 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して構いません)。
  4. histo.txtの1位の行からクラス名とインスタンス数を読み取り、/root/jvm/heap/top.txtにtop_class=<이름>とinstances=<n>の2行で書いてください(プレースホルダーはクラス名です)。
  5. java -Xmx64m -Dleak.retain=false -Dleak.iterations=3000 -cp /root/jvm/heap Leak > /root/jvm/heap/noleak.log 2>&1で、同じ割り当てを参照なしで実行してください。ログにdone iterations=3000があり、OutOfMemoryErrorがない必要があります。
  6. -XX:+ExitOnOutOfMemoryErrorを付けて実行し、出力を/root/jvm/heap/exit-on-oom.logに、終了コード(echo $?)を/root/jvm/heap/exit.txtに残してください。ログにTerminating due to java.lang.OutOfMemoryErrorがあり、exit.txtは3である必要があります。
  7. java -XX:+PrintFlagsFinal -versionのMaxHeapSizeの値と、-XX:MaxRAMPercentage=50を加えたときの値を読み取り、/root/jvm/heap/ram.txtにdefault_max_heap=<바이트>とhalf_max_heap=<바이트>の2行で書いてください(プレースホルダーはバイト数です)。
  8. /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になっている必要があります。

参考

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が含まれている必要があります。

2つのオプションはクラス名の前に置きます。ダンプは、例外がスローされる瞬間に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を実行して、2つのjcmdを呼び出してください。heap_dumpは先にFull GCを要求するので、到達可能なオブジェクトだけが残ります。

ヒストグラムの1行目を読む

histo.txtの1位の行から、クラス名とインスタンス数を/root/jvm/heap/top.txtにtop_class=<名前>とinstances=で書いてください。

1位の行は' 1:'で始まり、列は順位・インスタンス・バイト・クラス名の順です。名前は(java.base@…)の手前までです。awk '$1=="1:"{print $2, $4}'が2つの値を返します。

同じ割り当てでも参照がなければ

-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。最初のOutOfMemoryErrorでJVMがすぐに終了し、終了コードは3です(実測)。

コンテナのヒープのデフォルトを測る

java -XX:+PrintFlagsFinal -versionのMaxHeapSizeと、-XX:MaxRAMPercentage=50を加えたときのMaxHeapSizeを、/root/jvm/heap/ram.txtにdefault_max_heap=<バイト数>とhalf_max_heap=<バイト数>で書いてください。

「参考」セクションのawkで値を取り出し、2回目は-XX:MaxRAMPercentage=50を-XX:+PrintFlagsFinalの前に付けます。デフォルトが25%なので、2つ目の値は約2倍になります。

オプションをスクリプトに固定する

/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をスクリプトの1行目に書いてください。最後の行はecho $? > runbook-exit.txtです。