ファイナライズおよびファイナライザー

今日は、finalize()メソッドとオブジェクトの破壊を少し実験します。 初心者のJavaプログラマーでさえ、ガベージコレクターがオブジェクトを破棄することを決定したときにfinalize()が呼び出されるという考えを持っていますが、それでも予期しないことがあります。 たとえば、finalize()メソッドが非常に長い間機能する場合、アプリケーションはどうなりますか?



finalize()の公式ドキュメントには次のように書かれています:

ガベージコレクションがオブジェクトへの参照がこれ以上ないと判断したときに、オブジェクトのガベージコレクタによって呼び出されます。
これから、「凍結」ファイナライズ()がガベージコレクタースレッドを中断し、ビルドが停止すると想定できます。 実際には(少なくともHotSpot 1.6では)ガベージコレクターはfinalize()メソッドを直接呼び出しませんが、静的メソッドjava.lang.ref.Finalizer.register(Object)を呼び出すことにより、対応するオブジェクトを特別なリストに追加するだけです。 Finalizerクラスのオブジェクトは、finalize()を呼び出す必要のあるオブジェクトへの参照であり、次および前のFinalizerへのリンクを保存して、二重にリンクされたリストを形成します。



finalize()呼び出しは、個別のストリーム「Finalizer」(java.lang.ref.Finalizer.FinalizerThread)で直接行われます。これは、仮想マシンの起動時に作成されます(より正確には、Finalizerクラスをロードするときの静的セクション)。 finalize()メソッドは、ガベージコレクターによってリストに追加された順序で順番に呼び出されます。 したがって、finalize()がハングすると、Finalizerスレッドはハングしますが、ガベージコレクタはハングしません。 特に、これはfinalize()メソッドを持たないオブジェクトは適切に削除されますが、Finalizerスレッドがハングする、アプリケーションが終了する、またはメモリがなくなるまでキューに追加されるオブジェクトを意味します。



これを例で説明します。 オブジェクトがヒープ上の適切な場所を食べるクラスを作成しましょう:

static class BigObject { char[] tmp = new char[10000]; }
      
      





そして、このメインメソッドのようなものを書きます:

  public static void main(String... args) { int i=0; while(true) { new BigObject(); try { Thread.sleep(10); } catch( InterruptedException e ) {} if(i++%100==0) System.out.println("Total: "+Runtime.getRuntime().totalMemory()+ "; free: "+Runtime.getRuntime().freeMemory()); } }
      
      



各反復でオブジェクトを作成し、100回の反復で残りのメモリに関する情報を表示します。 テストを遅延させないようにJavaマシンのメモリを制限し、結果を確認します。

$ java -Xms16m -Xmx16m Test

Total: 16252928; free: 15965064

Total: 16252928; free: 14013136

Total: 16252928; free: 12011536

Total: 16252928; free: 14309664

Total: 16252928; free: 12308064

Total: 16252928; free: 14797440

Total: 16252928; free: 12795840

Total: 16252928; free: 15307784

...








メモリは適切に割り当てられ、解放されます。



ここで、オブジェクトが非常に長い間finalize()を実行しているクラスを作成します。

  static class LongFinalize { protected void finalize() throws Throwable { System.out.println("LongFinalize finalizer"); Thread.sleep(10000000); } }
      
      



ループの前に新しいLongFinalize()をmain()に追加します。 結果は次のようになります。

$ java -Xms16m -Xmx16m Test

Total: 16252928; free: 15965064

Total: 16252928; free: 14003496

Total: 16252928; free: 12001896

LongFinalize finalizer

Total: 16252928; free: 14290408

Total: 16252928; free: 12288808

Total: 16252928; free: 14777432

Total: 16252928; free: 12775832

Total: 16252928; free: 15286960

Total: 16252928; free: 13280880








ご覧のとおり、LongFinalize.finalize()を呼び出したにもかかわらず、ガベージコレクターは引き続き動作します。 次に、finishize()メソッドをBigObjectオブジェクトに追加します。これは重要ではないことを行います。

  static class BigObject { char[] tmp = new char[10000]; protected void finalize() throws Throwable { tmp[0] = 1; } }
      
      



今回は写真が異なります:

$ java -Xms16m -Xmx16m Test

Total: 16252928; free: 15965064

Total: 16252928; free: 14003496

Total: 16252928; free: 12001896

LongFinalize finalizer

Total: 16252928; free: 9996648

Total: 16252928; free: 7987144

Total: 16252928; free: 6459728

Total: 16252928; free: 4458128

Total: 16252928; free: 6357016

Total: 16252928; free: 4347352

Total: 16252928; free: 2331112

Total: 16252928; free: 329512

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

at Test$BigObject.<init>(Test.java:12)

at Test.main(Test.java:31)








メモリがさらに大きくなると、Finalize()がLongFinalize.finalizer()の前に実行されたBigObjectオブジェクトが破棄されたことに注意してください。



そのような状況では、finalize()を持たないオブジェクトのみが正しく削除されると上記で書きました。 実際、finalize()メソッドが空であれば十分です。 ガベージコレクターは、finalize()本体にコードがある場合にのみオブジェクトをFinalizerキューに追加します。 たとえば、空のfinalize()を持つ子クラスを作成できます。

  static class SubBigObject extends BigObject { protected void finalize() throws Throwable { } }
      
      



main()で子クラスのオブジェクトを作成します(新しいBigObject()を新しいSubBigObject()に置き換えます)。 ガベージコレクションが再び成功することがわかります。



したがって、空のfinalize()をサブクラス化して子オブジェクトのみを作成すると、オブジェクトの破棄を加速し、凍結されたファイナライザーストリームから保護することもできます。 もちろん、あなたが何をしているのかを知っておく必要があります:finalize()が書かれていれば、おそらく何かのためにそれを必要としているでしょう。 それでも、どうしても必要な場合を除き、finalize()を記述しないでください。 たとえば、抽象クラスInputStreamでは、finalize()を実行して、利便性を高めるためにclose()を呼び出すことができます。 実際、finalize()は、システムリソース(FileInputStreamなど)を直接操作する子クラスでのみ定義されます。 そして、例えば、BufferedInputStreamでは、FileInputStreamをラップする場合でもfinalize()は必要ありません。 ここで、過度の汎用性は有害です。 一部のライブラリの作成者が、抽象クラスで不必要にfinalize()を作成し、システムリソースを操作しない場合は、実装内で空の本体で再定義します。 実際、Finalizerがフリーズしなくても、解放されたオブジェクトの流れに単に対処できない場合があり、そのため、オブジェクトの削除とヒープの成長が大幅に遅くなります。



System.runFinalization()のようなものについても言わなければなりません。 この呼び出しは、2番目のスレッド "SecondaryFinalizer"を作成します。このスレッドは、同じキューからのオブジェクトに対してfinalize()も呼び出します。 同時に、System.runFinalization()を呼び出したスレッドは、現在利用可能なFinalizerキューが終了するまで待機します。 原則として、メインのファイナライザがフリーズした場合、OutOfMemoryからあなたを救うことができます。 SubBigObjectのないプログラムのバージョンに戻り、メモリが十分に残っていない場合にこの呼び出しを追加します。 混乱しないように、全文をお伝えします。

 public final class Test { static class LongFinalize { protected void finalize() throws Throwable { System.out.println("LongFinalize finalizer"); Thread.sleep(10000000); } } static class BigObject { char[] tmp = new char[10000]; protected void finalize() throws Throwable { tmp[0] = 1; } } public static void main(String... args) { int i=0; new LongFinalize(); while(true) { new BigObject(); try { Thread.sleep(10); } catch( InterruptedException e ) {} if(i++%100==0) System.out.println("Total: "+Runtime.getRuntime().totalMemory()+ "; free: "+Runtime.getRuntime().freeMemory()); if(Runtime.getRuntime().freeMemory()<1e6) System.runFinalization(); } } }
      
      





作業の結果を見てみましょう。

$ java -Xms16m -Xmx16m Test

Total: 16252928; free: 15965064

Total: 16252928; free: 14003496

Total: 16252928; free: 12001896

LongFinalize finalizer

Total: 16252928; free: 9996648

Total: 16252928; free: 7987144

Total: 16252928; free: 6459832

Total: 16252928; free: 4458232

Total: 16252928; free: 6357120

Total: 16252928; free: 4347456

Total: 16252928; free: 2331216

Total: 16252928; free: 239072

Total: 16252928; free: 11729800

Total: 16252928; free: 9717584

Total: 16252928; free: 7719416

Total: 16252928; free: 5710768

Total: 16252928; free: 3721880

Total: 16252928; free: 1710824

Total: 16252928; free: 11261488








メインのファイナライザー()がハングするという事実にもかかわらず、プログラムは存続します。 もちろん、長いファイナライズ()でキューに多くのオブジェクトがある場合、これはあなたを救いません、そして一般にプログラムのSystem.runFinalization()への明示的な呼び出しは何かが間違っていることを示す可能性が高いです。



All Articles