JavaやKotlinでOutOfMemoryErrorが発生!
どうやって調査するの?
ここではJavaやKotlinアプリケーションでOutOfMemoryErrorが発生した場合の調査方法について解説します。
アプリのパフォーマンスが思うように出ていない場合にもここで解説する方法が有効かもしれません。
メモリリーク発生時の調査方法
まずはJVMのメモリ状態を確認する
OutOfMemoryErrorは以下のような状況で発生します。
- JVMから利用できるネイティブメモリがない場合
- メタスペースがいっぱいになった場合
- ヒープがいっぱいになった場合(使用中のオブジェクトが多すぎてヒープとして設定容量を超えた)
- ガベージコレクション(GC)に時間がかかりすぎている場合
ここでは比較的調査しやすいJavaヒープの状態を把握します。
以下のコマンドでヒープ、メタスーペース、GC(ガベージコレクション)の様子を確認します。
jstatコマンドの詳細は「公式サイト」を参照して下さい。
ここでは簡単に各数値の内容を解説します。
$ jstat -gcutil <プロセスID> 1000
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
列 | 説明 |
---|---|
S0 | Survivor領域0の使用率(現在の容量に対するパーセンテージ) |
S1 | Survivor領域1の使用率(現在の容量に対するパーセンテージ) |
E | Eden領域の使用率(現在の容量に対するパーセンテージ) |
O | Old領域の使用率(現在の容量に対するパーセンテージ) |
M | メタスペースの使用率(現在の容量に対するパーセンテージ) |
CCS | 圧縮されたクラス領域の使用率(パーセンテージ) |
YGC | Young世代のGC(ガベージ・コレクション)イベントの数 |
YGCT | Young世代のガベージ・コレクション時間 |
FGC | フルGC(ガベージ・コレクション)イベントの数 |
FGCT | フル・ガベージ・コレクションの時間 |
GCT | ガベージ・コレクションの総時間 |
Javaヒープとガベージ・コレクション(GC)のおさらい
ヒープに溜まったごみオブジェクトを破棄する仕組みのことをガベージ・コレクション(GC)と呼びます。
ごみオブジェクトとは、アプリケーションで使わなくなったオブジェクトのことで、どこからも参照されていないオブジェクトになります。
Javaのヒープ領域はYoung領域とOld領域の2つの領域に分かれています。
Young領域はさらに、Eden・Survivor0・Survivor1空間に分かれています。
新しく作られたオブジェクトはまず、ヒープ中のyoung領域(Eden・Survivor空間)のうち、Eden空間に割り当てられます。
Eden空間が増えてくるとガベージ・コレクションが発生し、Eden空間のオブジェクトはSurvivor空間とOld領域のいずれかに移動します。このとき、Young領域に対するガベージ・コレクションではアプリケーションスレッドの停止が発生します。(マイナー・ガベージ・コレクションと呼ばれます)
Old領域への移動を繰り返していると、今度はOld領域がいっぱいになり、Old領域の不使用オブジェクトを解放するためにガベージ・コレクションが発生します。このとき、Old領域に対するガベージ・コレクションはフルガベージ・コレクションと呼ばれ、アプリケーションスレッドの停止時間も比較的長くなります。
JVMのメモリ状況を把握する
先ほどのjstatコマンドの結果に戻ります
$ jstat -gcutil <プロセスID> 1000
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
0.00 100.00 62.75 83.26 96.33 90.42 18 0.382 0 0.000 16 0.124 0.505
アプリを動かしながらjstatコマンドの出力結果を見ていきます。まず注目するのはYGCとFGCの回数です。
YGCとFGCのカウントが増えた時にそれぞれ、Young領域とOld領域のパーセンテージを確認します。
カウントが増えた時に、特にOld領域のオブジェクトが解放されて十分な空きが確保されているかを確認します。
ここでもし、十分な空きが確保されないようなら、まだ使われているオブジェクトがどんどん溜まっている(メモリリークしている)可能性が高いので、一度アプリケーションを見なおす必要があります。
アプリケーションを見直す必要性はわかりましたが、今の状態では具体的にどの部分を見直せば良いのかわかりません。
そこでヒープダンプを取得して、どこでメモリを消費しているのかを調査します。
ヒープヒストグラムを取得する
完全なヒープダンプを解析するには手間がかかります。
そのため、まずはヒープヒストグラムを使用してアプリケーション内で使われているオブジェクトの数とメモリ使用量を確認します。
以下のコマンドを使用します。
$ jcmd <プロセスID> GC.class_histogram
<pid>:
num #instances #bytes class name (module)
-------------------------------------------------------
1: 125594 100836184 [B (java.base@11.0.1)
2: 8835 13808528 [I (java.base@11.0.1)
3: 121838 2924112 java.lang.String (java.base@11.0.1)
4: 69810 2233920 java.util.concurrent.ConcurrentHashMap$Node (java.base@11.0.1)
5: 16641 1937432 java.lang.Class (java.base@11.0.1)
6: 28195 1709376 [Ljava.lang.Object; (java.base@11.0.1)
7: 40427 970248 groovyjarjarantlr4.v4.runtime.atn.ATNConfig
バイト列([B)やjava.lang.Stringなどは高い頻度で現れますので、ここではバイト列や文字列以外で上位に表示されているオブジェクトがないか確認します。
ヒープダンプを取得して解析する
先ほどのヒープヒストグラムでも分からず、より詳細な分析が必要な場合はヒープダンプを取得して解析します。
ヒープダンプは以下のコマンドで取得可能です。
# Full GCが実行されたうえでダンプを取得
$ jcmd <プロセスID> GC.heap_dump <ヒープダンプファイルの保存先パス>
# Full GCを実行させずにダンプを取得
$ jcmd <プロセスID> GC.heap_dump -all <ヒープダンプファイルの保存先パス>
取得したヒープダンプはバイナリファイルのため、そのままでは解析できません。
そこで、以下のいずれかのツールを使用して解析します。
- jhat
JDKに付属の分析ツールです。ヒープダンプを読み込むとHTTPサーバが起動し、シンプルなWebページでヒープの中身を分析可能です。 - VisualVM
高機能な分析ツールです。インスタンス数やサイズ、ドミネーター(dominator)の確認や、フィルターでの絞り込み機能など使えます。
公式サイトからダウンロードできます。
ヒープの中で保持メモリ量が多いオブジェクトをドミネーター(dominator)と呼びます。
少数のオブジェクトがヒープの多くを圧迫しているなどの状況がわかれば、これらのオブジェクトが保持される時間を短くしたり、生成回数を減らすなどの修正をすることで改善できるかもしれません。
まとめ
メモリリーク発生時の調査方法について解説しました。
もっと詳しく知りたい人はこの書籍に詳しい解説があります。
ガベージコレクションの種類やどんな動きをしているのかなど、すべてがここに・・・性能関連の話題も載ってます!