ガベージコレクタはオブジェクトが到達不能であると判断してから、そのメモリ領域を回収するまでの間に、オブジェクトのファイナライザメソッドを呼び出す。ファイナライザメソッドを実行することで、ストリーム、ファイル、ネットワーク接続といった、ガベージコレクタの通常動作では自動的に解放されないかもしれないリソースを解放することができる。
ファイナライザには様々な問題が存在するため、例外的場合にのみ使うべきである。以下にそれらの問題を挙げる。
- ファイナライザが実行されるタイミングはJVMに依存するので、決まった時に実行されなくてもよい。ファイナライズメソッドの動作について唯一保証されているのは、オブジェクトが到達不能になってから(ガベージコレクションの最初のサイクルで検出)、ガベージコレクタがそのメモリ領域を回収する(ガベージコレクションの2度目のサイクル)までの間に実行されるということだけである。ファイナライザの実行は、オブジェクトが到達不能になった後、どれだけ延期されるか分からない。したがって、ファイルハンドルをクローズするといった実行タイミングが問題になる処理をfinalize()メソッドの中で実行すると問題になる。
- JVMは、到達不能なオブジェクトのファイナライザを実行せずに終了するかもしれない。つまり、ファイナライザメソッドにおいて(オブジェクトの)重要な永続的状態を更新しようとしても、警告なしに失敗するかもしれない。同様に、Javaの言語仕様上、プロセスの終了時にファイナライザが実行される保証はない。System.gc(), System.runFinalization(), System.runFinalizersOnExit(), Runtime.runFinalizersOnExit()といったメソッドは、前述の保証が存在しないもしくは、安全性が確保できずデッドロックが発生する可能性があることから廃止されている。
- Java言語仕様§12.6.2「ファイナライザの起動は順序付けられていない」には次のように記されている。
プログラミング言語Javaでは、ファイナライズ・メソッドの呼出し順序を規定していない。このためファイナライザは様々な順序で呼び出され、並列的に呼び出される可能性もある。
これはつまり、実行に時間のかかるファイナライザは、実行キューの他のファイナライザの実行を遅延させる場合があるということである。さらに、ファイナライザの実行順序が規定されていないため、プログラムの不変条件を維持することが実質的に困難になる。
- ファイナライザの実行中に発生する例外は無視される。ファイナライザの中でスローされた例外がfinalize()メソッドの外に伝播する場合、プロセスはただちに処理を停止するため、例外はその目的を達成することができない。このように、ファイナライズの処理が強制終了させられると、以降のファイナライズがすべて実行されなくなるかもしれない。ファイナライザから例外がスローされた場合の動作はJava言語仕様に規定されておらず、したがってJVM実装にまかされている。
- メモリリークを引き起こすコーディング上の誤りは、オブジェクトを誤って到達可能なままの状態にしてしまう恐れがある。つまり、オブジェクトのファイナライザは決して呼び出されない。
- プログラマはfinalize()メソッドの中で、オブジェクトの参照を意図せず復活させてしまうことがある。その場合、ガベージコレクタは、オブジェクトを解放できるか再度判断しなければならない。さらに、finalizeメソッドは既に1度呼び出されているため、ガベージコレクタは2度目を呼び出すことはできない。
- ガベージコレクタの動作は通常、メモリの空き具合と使用状況に依存するが、他のリソースの使用状況には依存しない。したがって、メモリが容易に手に入る状況であれば、乏しいリソースが枯渇した場合にそれを適切にリリースするファイナライザが存在したとしても、リソースは枯渇してしまう可能性がある。リソース枯渇の正しい対処法については、「FIO04-J. 不要になったリソースは解放する」、 「TPS00-J. スレッドプールを使用しトラフィックの大量発生による急激なサービス低下を防ぐ」を参照のこと。
- ファイナライザがガベージコレクションを補助するという話は迷信にすぎない。逆に、ファイナライザは、ガベージコレクションにかかる時間を増大させ、メモリ空間のオーバーヘッドを増加させる。ファイナライザは、多くのオブジェクトの生存期間を延長することで、現代的な世代別(generational)ガベージコレクタの動作の邪魔をする。また、誤って実装されたファイナライザは、到達可能なオブジェクトをファイナライズしようとするかもしれない。これは無駄な動作であり、プログラムの不変条件を侵害するかもしれない。
- ファイナライザを使うと、たとえプログラムがシングルスレッドであっても、同期に関する問題が発生することがある。finalize()メソッドは、main()スレッドとは異なる単一のあるいは複数のスレッドのガベージコレクタによって呼び出されるが、必ずそうなる保証はない。ファイナライザをどうしても必要な場合は、クリアされるデータ構造が並行アクセスされても安全であるよう保護しなくてはならない。詳しくは、Hans J. Boehm によるJavaOne の発表を参照のこと。[Boehm 2005]
- ファイナライザの中でロック等の同期メカニズムを使用すると、デッドロックやリソースの枯渇が発生するかもしれない。その理由は、ファイナライザの実行順序のみならず、ファイナライザを実行するスレッドについても、一切保証したり制御したりすることができないからである。
こういった問題が存在するため、新規に開発するクラスでファイナライザを使うべきでない。
違反コード (スーパークラスの finalizer())
ファイナライザを使用するスーパークラスを拡張する際には、さらなる制限が課される。JDK 1.5およびそれ以前のバージョンで問題になった例について考えてみよう。以下の違反コードでは、Swing の JFrame オブジェクトのための16MBのバッファを割り当てている。JFrameのAPIにはfinalize()メソッドは実装されていないが、JFrame が継承する AWT.Frame には、finalize() が実装されている。MyFrameオブジェクトが到達不能になった場合、JFrameによって継承されたfinalize()メソッドの中でバイト配列のバッファを参照しているかもしれないため、ガベージコレクタはこのバッファのメモリ領域を回収することができない。つまり、クラスMyFrameに継承されたfinalize()メソッドの実行が少なくとも完了するまでの間は、バッファは存在し続けるに違いなく、次のガベージコレクションのサイクルまで回収できない。
class MyFrame extends JFrame { private byte[] buffer = new byte[16 * 1024 * 1024]; // 少なくとも2度のガベージコレクションサイクルの間存在し続ける }
適合コード (スーパークラスの finalizer())
スーパークラスでfinalize()メソッドを定義する場合、ただちにガベージコレクション可能なオブジェクトと、ファイナライザの終了に依存するオブジェクトは明確に分けるべきである。以下に示す解決法では、bufferは到達不能になれば直ちに回収することができる。
class MyFrame { private JFrame frame; private byte[] buffer = new byte[16 * 1024 * 1024]; // 分離している }
違反コード (System.runFinalizersOnExit())
以下の違反コードでは、System.runFinalizersOnExit()メソッドを使用し、ガベージコレクションを独自に実行している。このメソッドはスレッド安全性上の問題から廃止されていることに注意。詳細は「MET02-J. 非推奨(deprecated)あるいは廃止された(obsolete)クラスやメソッドを使用しない」を参照。
Java API [API 2006]には System クラスの runFinalizersOnExit()メソッドについて、以下のように記述されている。
終了時のファイナライズを有効または無効にする。これを実行することによって、自動的に呼び出されていないファイナライザを持つすべてのオブジェクトのファイナライザが呼び出され、Java Runtime の終了前に実行されるようになる。デフォルトでは終了時のファイナライズは無効である。
クラスSubClassはprotected finalize()メソッドをオーバーライドしており、クリーンアップ処理を行う。このファイナライザはsuper.finalize()を呼び出し、スーパークラスもファイナライズされることを保証している。BaseClassは、SubClassにおいてオーバーライドされているdoLogic()メソッドを呼び出してしまう。この呼出しによりSubClassへの参照が復活し、SubClass がガベージコレクションされるのを妨げるのみならず、SubClassのファイナライザが呼び出されなくなり、ファイナライザでリリースされるべきリソースがリリースされなくなる。「MET05-J. コンストラクタにおいてオーバーライド可能なメソッドを呼び出さない」で詳しく説明したように、サブクラスのファイナライザが重要なリソースをリリースした後で、スーパクラスからサブクラスのメソッドを呼び出すと、オブジェクトが不整合な状態になってしまうかもしれない。場合によっては、NullPointerExceptionを引き起こす。
class BaseClass { protected void finalize() throws Throwable { System.out.println("Superclass finalize!"); doLogic(); } public void doLogic() throws Throwable { System.out.println("This is super-class!"); } } class SubClass extends BaseClass { private Date d; // 可変インスタンスフィールド protected SubClass() { d = new Date(); } protected void finalize() throws Throwable { System.out.println("Subclass finalize!"); try { // リソースをクリアする d = null; } finally { super.finalize(); // BaseClass のファイナライザを呼び出す } } public void doLogic() throws Throwable { // ここで割り当てられたリソースは回収されない // オブジェクトの矛盾した状態 System.out.println( "This is sub-class! The date object is: " + d); // 'd' は既に null } } public class BadUse { public static void main(String[] args) { try { BaseClass bc = new SubClass(); // ファイナライザの動作をシミュレートする System.runFinalizersOnExit(true); } catch (Throwable t) { // エラー処理 } } }
このコードは以下を出力する
Subclass finalize! Superclass finalize! This is sub-class! The date object is: null
適合コード
Joshua Bloch [Bloch 2008] は、クラスを、その生存期間を越えたら使用不可能な状態にするような stop() メソッドを明示的に実装することを提唱している。クラスのプライベートフィールドを使ってそのクラスが使用不可能であることを表すことができる。すべてのクラスメソッドは、クラスに対する操作を行う前にまず、このフィールドをチェックしなくてはならない。この手法は、「OBJ11-J. コンストラクタが例外をスローする場合には細心の注意を払う」の適合コードで取り上げた「初期化フラグ」を使った手法と同じである。
例外
MET12-EX0: ネイティブコードを使ったプログラムを作成する場合にはファイナライザを使用してもよい。その理由は、他の言語で書かれたコードによって使用されるメモリをガベージコレクションすることは出来ないからであり、オブジェクトの生存期間も分からないからである。繰り返しになるが、ネイティブプロセスにおいて、すぐにリソースを解放する必要があるような重要なジョブを実行すべきではない。
finalize()をオーバーライドするサブクラスは、スーパークラスのファイナライザも明示的に呼び出さなくてはならない。finalize()が自動的に連鎖して呼び出されることはない。これを正しく行う方法を以下に示す。
protected void finalize() throws Throwable { try { //... } finally { super.finalize(); } }
よりコストのかかる解決法としては、アノニマスクラスを宣言し、finalize()メソッドがスーパークラスに対して実行されることを保証するというやり方もある。この解決法はfinal宣言されていないパブリッククラスに適用できる。「Finalizer Guardian を使用することによって、finalize をオーバーライドしたサブクラスが明示的に super.finalize を呼び出していない場合であっても、super.finazlie が強制的に呼び出されるようになる」[JLS 2005]。
public class Foo { // finalizeGuardian オブジェクトは、外側の Foo オブジェクトをファイナライズする。 private final Object finalizerGuardian = new Object() { protected void finalize() throws Throwable { // 外側の Foo オブジェクトをファイナライズ } }; //... }
ネイティブコードを使用する場合、ファイナライズの順序が問題になる場合がある。たとえば、オブジェクトAがオブジェクトBを(直接あるいはリフレクションにより)参照しており、Bが先にファイナライズされる場合、Aのファイナライザはネイティブコードのダングリングポインタを参照することになるかもしれない。ファイナライザを決まった順序で呼び出すためには、Aのファイナライザが終了するまでBが到達可能であることを保証する。これを実現するには、グローバルな状態変数にBへの参照を保存しておき、Aのファイナライザが実行されたらこれを削除する。java.lang.refの参照を使用するというやり方もある。
MET12-EX1: 「OBJ11-J. コンストラクタが例外をスローする場合には細心の注意を払う」で詳しく説明したように、ファイナライザ攻撃を防止するために、クラスに空のファイナライザを用意することがある。
リスク評価
ファイナライザを誤用すると、ガベージコレクションされるはずのオブジェクトが回収されなくなり、サービス運用妨害の脆弱性につながる恐れがある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
MET12-J | 中 | 中 | 中 | P8 | L2 |
関連する脆弱性
AXIS2-4163 には、Axis ウェブサービスフレームワークのfinalize()メソッドの脆弱性について記されている。ファイナライザが、クリーンアップ処理を行う前に誤ってsuper.finalize()を呼び出しているため、ガベージコレクタが実行されるとき、GlassFishの中でエラーが発生する。
関連ガイドライン
MITRE CWE | CWE-586. Explicit call to Finalize() |
CWE-583. finalize() method declared public | |
CWE-568. finalize() method without super.finalize() |
参考文献
[API 2006] | finalize() |
[Bloch 2008] | Item 7. Avoid finalizers |
[Boehm 2005] | |
[Coomes 2007] | "Sneaky" Memory Retention |
[Darwin 2004] | Section 9.5, The Finalize Method |
[Flanagan 2005] | Section 3.3, Destroying and Finalizing Objects |
[JLS 2005] | §12.6, Finalization of Class Instances |
翻訳元
これは以下のページを翻訳したものです。
MET12-J. Do not use finalizers (revision 118)