Javaはどのように動くのか~図解でわかるJVMの仕組み

第2回 ヒープが再利用される仕組みを理解する

この記事を読むのに必要な時間:およそ 2 分

なぜGC後でもヒープの使用量が上がってしまうか~メモリリーク

GCが行われると不要なオブジェクトが削除されますが,ヒープの使用量を監視していると,GC後のヒープ使用量が右肩上がりに上がっていくことがあります。このような場合,プログラムの実行に今後必要ないにもかかわらず,参照が残ってしまい,GCの対象とならないオブジェクトが増加している可能性があります。

このようなことをメモリリークと言います。

図6 メモリリーク

図6 メモリリーク

このようなオブジェクトが1つ2つある程度では問題がないように思えますが,塵が積もれば山となってしまい,ヒープが使用できる領域が圧迫されてしまいます。

メモリリークは,コレクションクラスと呼ばれるListインターフェースやMapインターフェースなどの実装クラスがアプリケーションの広い範囲で使用される場合には,コレクションクラスからオブジェクトの消し忘れによって発生する可能性が高く,メモリリークを発見することは非常に困難です。

結果として,プログラムで使えるはずのヒープが少なくなり,GCが多発します。そのため,発生が確認できた場合は優先的にプログラムを修正しましょう。

図7 メモリリークによるヒープの圧迫

図7 メモリリークによるヒープの圧迫

オブジェクトがGCに回収される前に処理を行うには~ファイナライザ

プログラムを作っていると,オブジェクトを回収する前にログを出力させたり,リソースを解放したりなど何か処理を行わせたいことがあるかもしれません。

そのような場合に利用するのが「ファイナライザ」です。

オブジェクト指向経験者の方には,オブジェクトを削除する際に実行される「デストラクタ」に似たものと考えていただいてかまいません。

ファイナライザは,すべてのクラスの親クラスであるObjectクラスに実装されているfinalize()メソッドをオーバーライドすることで実装できます。

ファイナライザは,オブジェクトへの参照がなくなってから,オブジェクトがGCに回収される前に,ファイナライザスレッドで実行されます。

ファイナライザスレッドは,Objectクラスで実装されたfinalize()メソッドを実行しますが,基本的には何も処理を行わないように実装されています。しかし,サブクラスでオーバーライドされている場合は,オーバーライドされたfinalize()メソッドを実行します。

図8 ファイナライザ

図8 ファイナライザ

そのとき,以下のような場合には,不要なオブジェクトがファイナライザで処理待ちとなり,ファイナライザスレッドの処理がGCでオブジェクトを回収する速度に追いつけなくなります。

  • 処理を必要とするfinalize()メソッドをサブクラスでオーバーライドしたオブジェクトが多い場合
  • finalize()メソッドで時間のかかる処理をしている場合

結果として,ヒープの解放も遅くなり,ヒープが枯渇するとOOMEが発生してしまいます。

図9 ファイナライザが終わらない

図9 ファイナライザが終わらない

C++では,先述のように,delete演算子で削除のタイミングをプログラマが制御できます。しかし,ファイナライザは「GCで回収される前に実行される」としか決まっていないため,いつどのタイミングで呼ばれるかわからず,実行される順番も保証されていません。

そのため,データベースへのコネクションの解放や,ファイルの解放など,リソースの解放処理をファイナライザで行うと,リソースがいつ解放されるかわからず,リソースを予期せずに占有してしまうことがあります。このようなリソースが解放されるタイミングに影響される設計は避ける必要があります。

図10 ファイナライザによるリソースの解放

図10 ファイナライザによるリソースの解放

次回は,どのような場合にGCが問題になるのかについて解説します。お楽しみに。

著者プロフィール

伊藤智博(いとうちひろ)

日本オラクル(株)コンサルティングサービス統括所属。アプリケーションアーキテクトやM/Wコンサルとして,Javaに限らずさまざまな言語での開発/支援を経験。アプリ ケーションだけでなくミドルウェア,データベース,OS,ハードウェアにも興味アリ。現在,以下のコミュニティの活動に協力している。

Japan Oracle User Group(JPOUG)
Oracle LOVERS

ブログ:http://chiroito.blogspot.jp/
Twitter:@chiroito