本連載は、日立製作所が提供するアプリケーションサーバ「Cosminexus」の開発担当者へのインタビューを通じて、Webシステムにおける、さまざまな問題/トラブルの解決に効くノウハウや注意点を紹介していく。現在起きている問題の解決や、今後の開発のご参考に(編集部)
今回は、Java開発者であれば、誰もが遭遇するメモリ不足エラー「OutOfMemoryError」に対する基本的な切り分け方法と、OutOfMemoryError発生につながる危険なケースを具体例を挙げて解説する。
Javaのメモリは大きく分けて「Javaヒープ」「Cヒープ」の2種類がある。Javaヒープは、Javaオブジェクトなどのプログラムのリソースを格納する領域、Cヒープは、JavaVM自身のリソースを格納する領域である。それぞれの領域が不足した場合、メモリ不足エラー「OutOfMemoryError」が発生する。
まずは、メモリ不足がJavaヒープとCヒープのどちらで発生したかを切り分けておこう。
OutOfMemoryErrorがスローされ、JavaVMの実行が継続している場合には、Javaヒープが不足している可能性が高い。Javaヒープ不足かどうかを確定させるために、スローされたOutOfMemoryErrorのトレースを確認しよう。
java.lang.OutOfMemoryError: Java heap space <=======【*1】 at java.nio.CharBuffer.wrap(CharBuffer.java:350) <=======【*2】 at java.nio.CharBuffer.wrap(CharBuffer.java:373) at java.lang.StringCoding$StringDecoder.decode(StringCoding.java:138) at java.lang.StringCoding.decode(StringCoding.java:173) at java.lang.String.<init>(String.java:444) at com.sun.tools.javac.zip.ZipFileIndex$ZipDirectory.readEntry(ZipFileIndex.java:805) at com.sun.tools.javac.zip.ZipFileIndex$ZipDirectory.buildIndex(ZipFileIndex.java:720) at com.sun.tools.javac.zip.ZipFileIndex$ZipDirectory.access$000(ZipFileIndex.java:652) at com.sun.tools.javac.zip.ZipFileIndex.checkIndex(ZipFileIndex.java:261) ……
OutOfMemoryErrorが「Java heap space」とともに出力され(【*1】)、OutOfMemoryErrorの発生位置がjavaソース【*2】であれば、Javaヒープ不足で確定である。
なお、Cosminexusの場合は、デフォルトでコンソールに以下のように出力されるという。
java.lang.OutOfMemoryError occurred. JavaVM aborted because of specified -XX:+HitachiOutOfMemoryAbort options.
OutOfMemoryErrorがスローされず、Java VMがクラッシュしている場合には、Cヒープ不足で確定だ。この場合、コンソールには以下のように出力される。
# # An unexpected error has been detected by Java Runtime Environment: # # java.lang.OutOfMemoryError: requested 64000 bytes for GrET in <=======【*3】 # /BUILD_AREA/jdk6_04/hotspot/src/share/vm/utilities/growableArray.cpp. Out of # swap space? # # Internal Error (allocation.inline.hpp:42), pid=20594, tid=2903325616 # Error: GrET in # /BUILD_AREA/jdk6_04/hotspot/src/share/vm/utilities/growableArray.cpp # # Java VM: Java HotSpot(TM) Client VM (10.0-b19 mixed mode linux-x86) # An error report file with more information is saved as: # /tmp/hs_err_pid20594.log # # If you would like to submit a bug report, please visit: # http://java.sun.com/webapps/bugreport/crash.jsp #
OutOfMemoryErrorが「requested <n> bytes for <message>」とともに出力されるのが特徴である(【*3】)。
出力項目 | 説明 |
---|---|
n | メモリ確保要求サイズ |
message | 調査に必要な内部情報 |
まれに、Cヒープ不足でも、OutOfMemoryErrorがスローされる場合がある。この場合は、OutOfMemoryErrorのトレースが、以下のようになる。
java.lang.OutOfMemoryError at java.io.RandomAccessFile.readBytes(Native Method) <======【*4】 at java.io.RandomAccessFile.read(RandomAccessFile.java:322) at java.io.RandomAccessFile.readFully(RandomAccessFile.java:381) at com.sun.tools.javac.zip.ZipFileIndex$ZipDirectory.findCENRecord(ZipFileIndex.java:702) at com.sun.tools.javac.zip.ZipFileIndex$ZipDirectory.<init>(ZipFileIndex.java:665) at com.sun.tools.javac.zip.ZipFileIndex.checkIndex(ZipFileIndex.java:260) at com.sun.tools.javac.zip.ZipFileIndex.<init>(ZipFileIndex.java:209) at com.sun.tools.javac.zip.ZipFileIndex.getZipFileIndex(ZipFileIndex.java:115) at com.sun.tools.javac.util.DefaultFileManager.openArchive(DefaultFileManager.java:645) at com.sun.tools.javac.util.DefaultFileManager.listDirectory(DefaultFileManager.java:325) at com.sun.tools.javac.util.DefaultFileManager.list(DefaultFileManager.java:872) at com.sun.tools.javac.jvm.ClassReader.fillIn(ClassReader.java:2077) at com.sun.tools.javac.jvm.ClassReader.complete(ClassReader.java:1781
OutOfMemoryErrorのトレースの発生位置がNative Method(【*4】)となっている。これはJava APIのnative実装部分でメモリ確保に失敗しているためである。
「OutOfMemoryError」発生につながる危険なケースを以下に、いくつか紹介する。今回は、特に調査の難しいCヒープ不足に絞って解説する。これらの事例を参考にして、「OutOfMemoryError」発生を防ごう。
スレッドはスタック領域としてCヒープを消費する。スレッド当たりのスタック領域サイズは、以下の通りである。
32bit OS/64bit OS | スレッド当たりのスタック領域サイズ |
---|---|
32bit OS | 512Kbytes |
64bit OS | 1Mbytes |
スレッドは、アプリケーションから明示的に作成する場合だけでなく、Java APIから暗黙的に作成する場合もある。このため、想定外にスレッドが作成されていないか、テスト段階で確認し、Cヒープ上限値を超えることがないかどうか確かめておくのがよいだろう。
スレッド数の確認は、スレッドダンプを採取したり、JDKのjconsoleなどの監視ツールを使ったりすることで可能だ。
java.io.FileクラスのdeleteOnExitメソッドはアプリケーション終了時にファイルを削除してくれる便利なメソッドだ。しかし、メソッドで削除するファイルの情報をずっと保持するため、使えば使うほどメモリを消費する。
JDK 5までは、この情報の保持先はCヒープとなっている。使いすぎに注意しないと、Cヒープを圧迫することになってしまう。
なお、JDK 6からは情報の保持先がJavaヒープになっている。このAPIを多用している場合、JDK 5までとJDK 6からとで消費するヒープが異なる点も要注意だ。JavaVMのバージョンアップなどのときに、メモリ使用サイズを確認しておくのがよいだろう。
JDKバージョン | スレッド当たりのスタック領域サイズ |
---|---|
〜5.0 | Cヒープ |
6.0〜 | Javaヒープ |
ファイル入出力や通信関連のAPIは、最終的にはネイティブなシステムコールで実現されている。そのため、入出力対象データは、いったんCヒープにコピーされた後、システムコールに渡される。コピー先のバッファは、8Kbytes未満の場合は当該スレッドのスタック上に取られることになるが、それを超える場合は、Cヒープに一時的に確保される。
I/Oが完了すれば、その領域は解放される。しかし、あまりに大きなI/Oを行うと、一時的にCヒープを圧迫することになるので、気を付けよう。
バッファサイズ | バッファの確保先 |
---|---|
8Kbytes未満 | スタック |
8Kbytes以上 | Cヒープ |
java.util.zipパッケージのDeflaterクラスやInflaterクラスでは、コンストラクタの処理でCヒープ領域を確保する。確保されたCヒープ領域は、オブジェクトがGCされてfinalize()が実行されるか、end()メソッドを明示的に実行するまでは解放されない。
そのため、これらのクラスのオブジェクトを大量に作成して保持していると、Cヒープを圧迫することになるので、気を付けよう。
java.nio.ByteBufferクラスのallocateDirectメソッドや、java.nio.channels.FileChannelクラスのmapメソッドによりBufferオブジェクトを生成する場合、引数で指定したサイズの領域をmmapシステムコールにより確保する。
そのため、大きなサイズを指定したり、大量のBufferインスタンスを生成した場合にメモリが圧迫される可能性があります。この領域はBufferオブジェクトがガベージコレクトされるまで解放されないので、気を付けよう。
Copyright © ITmedia, Inc. All Rights Reserved.