OutOfMemoryErrorの原因と対応(3)
OutOfMemoryError回避のためのJavaコーディング – 前編と後編でOutOfMemoryErrorの典型的な発生パターンを3つ紹介した。
- (A)サイズオーバー型
- 巨大な領域確保によって一気にヒープの最大サイズをオーバー
- (B)メモリリーク型
- 開放されないオブジェクトが溜まり続けることで使用中メモリが徐々に増加し、メモリが枯渇
- (C)マルチスレッド型
- 長時間処理により”死んだ”スレッドがメモリを食い尽くす
じゃあOutOfMemoryErrorが発生したときに、どのパターンのエラーに該当するのか?これを見抜くための手がかりを最後の話題にしたい。
原因究明の手がかりは・・・?
OutOfMemoryErrorの原因究明。見るべきポイントはいくつかある。
再現するかどうかをチェック
まずは、特定のURLにアクセスした際に高頻度でOutOfMemoryErrorが発生するかどうか、という点。これはアクセスログとスタックトレースを照らし合わせれば分かる。
再現しやすいタイプは、前編で登場したサイズオーバー型だと思う。怪しいURLが分かってしまえば、あとは繰り返し処理を中心に大量メモリを使用している箇所を探す。スタックトレースが出力されているのであれば、それも参考になる。
サイズオーバー型のサンプルで出力されたスタックトレースはこんな感じ。
java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:2882) at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:100) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:390) at java.lang.StringBuffer.append(StringBuffer.java:224) at CsvServlet.doGet(CsvServlet.java:16) at javax.servlet.http.HttpServlet.service(HttpServlet.java:689) at javax.servlet.http.HttpServlet.service(HttpServlet.java:802)
StringBuffer#appendメソッドを呼び出した後、StringBufferクラスの内部処理中にOutOfMemoryErrorが発生していることがわかる。
ただし注意が必要なのは、OutOfMemoryErrorが発生した部分が大量メモリを使用しているとは限らないということ。大量のメモリ消費により、空き領域が少ない状態になっていると、ごく少量のメモリ確保もできず、その際にOutOfMemoryErrorが発生するケースもあったりする。
だから、OutOfMemoryErrorが発生したリクエストより少し前のリクエストに原因があることもあるのだ。
発生頻度が徐々に増えてきているかどうかをチェック
ポツリポツリとOutOfMemoryErrorが発生しつつもサイトは動作し続け、次第にOutOfMemoryErrorが増えていくのならメモリリークを疑ったほうがいい(前編のメモリリーク型)。
メモリリークの場合、アクセスログやスタックトレースを見ても原因箇所を特定できない。解決方法としてはJVMのヒープダンプを取得し、ヒープダンプを解析することにより、巨大化したオブジェクトを調べる。そのオブジェクトがメモリリークした箇所になる。
ヒープダンプを出力する方法は、JVMのベンダーやバージョンによってまちまち。例えば、SunのJDK6の場合は、あらかじめVMの起動引数に -agentlib:hprof=heap=sites オプションを指定してサーバーを起動しておく。Tomcatだったら次のようなファイルをTOMCAT_HOME/bin/setbin.shに置いておく。
JAVA_OPTS="$JAVA_OPTS -agentlib:hprof=heap=sites"
で、ヒープダンプを取得したいタイミングでQUITシグナルを送信する。
$ kill -QUIT [JavaプロセスID]
するとjava.hprof.txtのようなテキストファイルがカレントディレクトリに出力される。テキストファイルを見ると、JVM上にどのクラスのインスタンスが何個存在していて、どのくらいメモリを消費しているのかが分かる。
一方、IBMのJVMならHeapDump()メソッドをコールする。次のようなjspを作っておいて、ヒープダンプが欲しいときにアクセスすればOK。
<%@ page contentType="text/plain"%> <% com.ibm.jvm.Dump.HeapDump(); %> heapdump output ok
ヒープダンプはJVMを起動したユーザーのカレントディレクトリに出力される。分からなくなったときは、/tmp/dump_locations ファイルに出力パスが記録されているから、このファイルを見れば安心。
あとIBMのヒープダンプは、専用解析ツールHeapAnalyzerで解析できる。これは結構よくできたGUIツールだから使いやすい。
しかしどちらの方法でも、OutOfMemoryErrorが発生してからではヒープダンプが出力できないことに注意が必要だ。でも大丈夫。SunのJDKなら、JVM起動オプションに「-XX:+HeapDumpOnOutOfMemoryError」を指定すれば、OutOfMemoryError発生時にヒープダンプを自動的に出力してくれる便利な機能があるのだ。
とはいっても、ヒープダンプ出力中はJVMが完全に停止してしまう。またダンプファイルのサイズが数百MBになることも・・・。運用サイトでヒープダンプを出力するのは、いろいろ影響が大きくて大変だった経験がある。
レスポンスタイムをチェック
後編で書いたマルチスレッド型だと、特定のURLに対してレスポンスタイムが徐々に遅くなっていく特徴がある。また、リクエスト数に比べてJavaスレッド数が多すぎるという傾向もある。
こういう現象が確認できたときは、該当するURL内の処理で、タイムアウトが適切に設定されているか、処理に長時間を要している箇所はないかをチェックする。
Apacheのデフォルト設定ではアクセスログにレスポンスタイムは出力されないから、運用サイトでは最初からレスポンスタイムを出力するように設定しておくと、原因究明の役に立つだろう。
LogFormat "%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i" %d" combined
まとめ
OutOfMemoryErrorの調査方法を知っておくと、いざというときに非常に役に立つ。その調査に不可欠なのがアクセスログ、アプリケーションログ(スタックトレース)、ヒープダンプの3つ。
特にアプリケーションログには、あらかじめ日時とスレッド名が出力されるようにしておきたい。スタックトレースだけではアクセスログとの照らし合わせが難しくなりがち。あと、どのスレッドで問題が発生しているのかも分かりやすくなる。運用サイトではログにも気を配っておきたい。
(参考サイト)