JVM メモリリーク時の対応
[最終更新] (2019/06/03 00:44:43)
最近の投稿
注目の記事

概要

JVM 実行中に

java.lang.OutOfMemoryError: Java heap space

または

java.lang.OutOfMemoryError: GC overhead limit exceeded

が出力された場合の対応方法のひとつをまとめます。

jstat コマンドで JVM のメモリ使用状況を把握

Java ヒープ・メモリの構造」や「Javaのヒープ・メモリ管理の仕組みについて」にまとめられているように、JVM のメモリ管理は以下のようになっています。

| Cヒープ | スレッドスタック | Javaヒープ |
  • Cヒープ → JVM が OS のネイティブライブラリを実行する際に使用
  • スレッドスタック → スタック領域
  • Javaヒープ → 変数やオブジェクトなどを格納する領域 (Xms と Xmx でサイズを指定します)

概要に記載した Out Of Memory Error (OOME) はいずれも Java ヒープが不足した場合に発生します。単純にアプリケーションの処理上必要なサイズが満たせていない場合は Xms と Xmx で割り当てれば解決します。そうではなく、処理上不要なはずのオブジェクトへの参照がループ毎に削除されずに蓄積されていきメモリリークが発生している場合はアプリの修正が必要です。メモリリークしているかどうかの判断には jstat コマンドが利用できます。

ガベージコレクション (GC) について

Java ヒープは更に以下のように細分化されています。JVM の GC には二種類あります。

| -----------New---------- | ---------------Old--------------- | Permanent |
| Eden | From/To | To/From |                                   |           |
  1. オブジェクトが生成されると Eden に格納される
  2. Eden が満杯になると Scavenge GC が実行されて参照されていれば To に移される
  3. From と To のラベルが入れ替わる
  4. Eden が満杯になると Scavenge GC が実行されて Eden と From の参照されていないオブジェクトは To に移される。これを繰り返す
  5. From/To と To/From を行ったり来たりしながら所定の回数 (MaxTenuringThreshold) を越えても参照されているオブジェクトは Old に移される
  6. Old 領域も満杯になってくると Full GC が実行される

補足

  • From/To と To/From はまとめて Survivor ともよばれる
  • Old は Tenured (身分保証のある) ともよばれる
  • New + Old + Permanent の初期値は Xms で指定できる (Xmx と同じ値にしておくことが推奨される)
  • New + Old + Permanent の最大値は Xmx で指定できる
  • New のサイズは Xmn で指定できる (Xmx の 25% から 50% の範囲で設定。短命なオブジェクトが多い場合は 50% にして YGC の回数を減らし Old への移動を避けて結果的に FGC 回数も減らす)

jstat コマンドで GC の様子をみる

jps コマンドで JVM の PID を調査してから、以下のコマンドを実行します。

$ jstat -gcutil -h5 <pid> 1000

以下のような出力が 1000 ミリ秒ごとに得られます。

S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
 0.00  97.02  70.31  66.80  95.52  89.14      7    0.300     0    0.000    0.300
 0.00  97.02  86.23  66.80  95.52  89.14      7    0.300     0    0.000    0.300
 0.00  97.02  96.53  66.80  95.52  89.14      7    0.300     0    0.000    0.300
91.03   0.00   1.98  68.19  95.89  91.24      8    0.378     0    0.000    0.378
91.03   0.00  15.82  68.19  95.89  91.24      8    0.378     0    0.000    0.378
91.03   0.00  17.80  68.19  95.89  91.24      8    0.378     0    0.000    0.378
91.03   0.00  17.80  68.19  95.89  91.24      8    0.378     0    0.000    0.378
  • -gcutil GC の統計情報を出力
  • -h5 5 行ごとにヘッダーを出力
  • S0/S1 Survivor (From/To または To/From) 利用率
  • E Eden 利用率
  • O Old 利用率
  • M, CCS Permanent 利用率
  • YGC Scavenge GC (Young GC) が JVM 起動時から今までに発生した回数
  • YGCT Scavenge GC (Young GC) のために JVM 起動時から今までに要した秒数
  • FGC Full GC が JVM 起動時から今までに発生した回数
  • FGCT Full GC のために JVM 起動時から今までに要した秒数
  • GCT YGCT + FGCT

GC overhead limit exceeded とは何か

java.lang.OutOfMemoryError: Java heap space は文字通り Java ヒープの不足で発生します。一方で java.lang.OutOfMemoryError: GC overhead limit exceeded は、こちらのページまとめられているように以下の条件で発生します。潜在的に java.lang.OutOfMemoryError: Java heap space が発生する状況であるのだから、GC して無駄にユーザーを待たせるよりもさっさと落ちてしまいましょう、といった感じのエラーです。

  • ほぼすべての CPU 時間を GC のために使用するようになってしまった
  • GC を実行したにも関わらず Java ヒープの空きが Xmx の 2% 以下しかない

ヒープダンプして解析

GC の状況をみて、徐々に使用率が上昇しているようであればメモリリークしている可能性があります。jmap コマンドでヒープダンプを取得して Eclipse スタンドアロン Memory Analyzer (MAT) で解析を行い、メモリを消費しているオブジェクトを特定しましょう。インストールして起動したら「Open a Heap Dump」→「Leak Suspects Report」→「Problem Suspect N」を開いて可能性のあるオブジェクトを特定し、使い終わったら null を代入するなどコードを修正します。

メモリリークするサンプルコード

import java.util.Random
import scala.collection.mutable.Map

object Main {
  def main(args: Array[String]) : Unit = {
    val map = Map[Int, String]()
    val r = new Random
    while(true) {
      map += (r.nextInt() -> "value")
      Thread.sleep(1)
    }
  }
}

ヒープダンプの取得

この続きが気になる方は

JVM メモリリーク時の対応

残り文字数は全体の約 11 %
tybot
100 円
関連ページ
    概要 JVM メモリリークでは JDK の jstat や jmap で原因を調査できます。C/C++ では valgrind の Memcheck ツールが利用できます。valgrind には複数のツールが含まれており既定のツールが Memcheck です。他のツールを利用する場合は --tool オプションで指定します。
    概要 メモリリーク時に JVM の jmap や C/C++ の Valgrind で調査できるのと同様に、Python では objgraph が便利です。 sudo apt install graphviz python -m pip install xdot python -m pip install objgraph