オブジェクトのファイナライズとクリーンアップ

3か月前、私はオブジェクトの設計に関するミニシリーズの記事を開始し、オブジェクトの寿命の初めに適切な初期化に焦点を当てた設計原則について説明しました。この設計手法の記事では、オブジェクトの寿命が尽きたときに適切なクリーンアップを保証するのに役立つ設計原則に焦点を当てます。

なぜクリーンアップするのですか?

Javaプログラム内のすべてのオブジェクトは、有限のコンピューティングリソースを使用します。最も明らかに、すべてのオブジェクトは、ヒープに画像を格納するために何らかのメモリを使用します。 (これは、インスタンス変数を宣言しないオブジェクトにも当てはまります。各オブジェクトイメージには、クラスデータへの何らかのポインタが含まれている必要があり、実装に依存する他の情報も含めることができます。)ただし、オブジェクトはメモリ以外の有限のリソースを使用する場合もあります。たとえば、一部のオブジェクトは、ファイルハンドル、グラフィックコンテキスト、ソケットなどのリソースを使用する場合があります。オブジェクトを設計するときは、システムがそれらのリソースを使い果たしないように、オブジェクトが使用する有限のリソースを最終的に解放することを確認する必要があります。

Javaはガベージコレクションされた言語であるため、オブジェクトに関連付けられたメモリを解放するのは簡単です。あなたがする必要があるのは、オブジェクトへのすべての参照を手放すことです。 CやC ++などの言語で必要なように、オブジェクトを明示的に解放することを心配する必要がないため、同じオブジェクトを誤って2回解放してメモリが破損することを心配する必要はありません。ただし、オブジェクトへのすべての参照を実際に解放することを確認する必要があります。そうしないと、オブジェクトを明示的に解放するのを忘れたときにC ++プログラムで発生するメモリリークと同じように、メモリリークが発生する可能性があります。それでも、オブジェクトへのすべての参照を解放する限り、そのメモリを明示的に「解放」することを心配する必要はありません。

同様に、不要になったオブジェクトのインスタンス変数によって参照される構成オブジェクトを明示的に解放することを心配する必要はありません。不要なオブジェクトへのすべての参照を解放すると、そのオブジェクトのインスタンス変数に含まれている構成オブジェクト参照が事実上無効になります。現在無効になっている参照がそれらの構成オブジェクトへの唯一の残りの参照である場合、構成オブジェクトはガベージコレクションにも使用できます。ケーキですよね?

ガベージコレクションのルール

ガベージコレクションは確かにJavaでのメモリ管理をCやC ++よりもはるかに簡単にしますが、Javaでプログラミングするときにメモリを完全に忘れることはできません。Javaでのメモリ管理について考える必要がある場合を知るには、Java仕様でガベージコレクションがどのように扱われるかについて少し知る必要があります。

ガベージコレクションは必須ではありません

最初に知っておくべきことは、Java仮想マシン仕様(JVM仕様)をどれほど熱心に検索しても、コマンドを実行する文を見つけることができないということです。すべてのJVMにはガベージコレクターが必要です。 Java仮想マシン仕様により、VM設計者は、ガベージコレクションを使用するかどうかを決定するなど、実装でメモリを管理する方法を決定する際に大きな余裕が生まれます。したがって、一部のJVM(ベアボーンスマートカードJVMなど)では、各セッションで実行されるプログラムが使用可能なメモリに「収まる」必要がある可能性があります。

もちろん、仮想メモリシステムであっても、常にメモリが不足する可能性があります。JVM仕様には、JVMで使用できるメモリの量は記載されていません。JVMメモリを使い果たしたときはいつでも、をスローする必要あるとだけ述べてますOutOfMemoryError

それでも、Javaアプリケーションにメモリを使い果たすことなく実行できる最高のチャンスを与えるために、ほとんどのJVMはガベージコレクターを使用します。ガベージコレクターは、ヒープ上の参照されていないオブジェクトによって占有されているメモリを再利用するため、新しいオブジェクトがメモリを再び使用でき、通常、プログラムの実行時にヒープのフラグメントを解除します。

ガベージコレクションアルゴリズムが定義されていません

JVM仕様にないもう1つのコマンドは、ガベージコレクションを使用するすべてのJVMがXXXアルゴリズムを使用する必要があることです。各JVMの設計者は、実装でガベージコレクションがどのように機能するかを決定できます。ガベージコレクションアルゴリズムは、JVMベンダーが競合他社よりも実装を改善するために努力できる領域の1つです。これは、次の理由でJavaプログラマーとして重要です。

一般に、JVM内でガベージコレクションがどのように実行されるかがわからないため、特定のオブジェクトがいつガベージコレクションされるかはわかりません。

だから何?あなたは尋ねるかもしれません。オブジェクトがガベージコレクションされるときに気になる理由は、ファイナライザーと関係があります。(ファイナライザーは、finalize()voidを返し、引数をとらないという名前の通常のJavaインスタンスメソッドとして定義されます。)Java仕様では、ファイナライザーについて次のことが約束されています。

ファイナライザーを持つオブジェクトが占有しているメモリを再利用する前に、ガベージコレクターはそのオブジェクトのファイナライザーを呼び出します。

オブジェクトがいつガベージコレクションされるかはわかりませんが、ファイナライズ可能なオブジェクトはガベージコレクションされるためファイナライズされることはわかっているので、次の大規模な控除を行うことができます。

オブジェクトがいつファイナライズされるかはわかりません。

この重要な事実を脳に刻印し、Javaオブジェクトの設計に通知できるようにする必要があります。

避けるべきファイナライザー

ファイナライザーに関する経験則は次のとおりです。

正確さが「タイムリーな」ファイナライズに依存するようにJavaプログラムを設計しないでください。

言い換えれば、特定のオブジェクトがプログラムの実行期間中の特定の時点でファイナライズされない場合に破損するプログラムを作成しないでください。このようなプログラムを作成すると、JVMの一部の実装では機能するが、他の実装では失敗する場合があります。

非メモリリソースを解放するためにファイナライザーに依存しないでください

このルールに違反するオブジェクトの例は、コンストラクターでファイルを開き、finalize()メソッドでファイルを閉じるオブジェクトです。このデザインはきちんとしていて、きちんとしていて、対称的に見えますが、潜在的に陰湿なバグを作成します。 Javaプログラムは通常、自由に使えるファイルハンドルの数が限られています。これらのハンドルがすべて使用されている場合、プログラムはそれ以上ファイルを開くことができなくなります。

このようなオブジェクト(コンストラクターでファイルを開き、ファイナライザーでファイルを閉じるオブジェクト)を使用するJavaプログラムは、一部のJVM実装で正常に機能する場合があります。このような実装では、ファイナライズは、十分な数のファイルハンドルを常に使用できるようにするのに十分な頻度で発生します。ただし、同じプログラムが別のJVMで失敗する可能性があり、そのガベージコレクターは、プログラムのファイルハンドルが不足するのを防ぐのに十分な頻度でファイナライズされません。または、さらに陰湿なことに、プログラムは現在すべてのJVM実装で機能する可能性がありますが、数年後(およびリリースサイクル)のミッションクリティカルな状況では失敗します。

その他のファイナライザーの経験則

JVM設計者に残された他の2つの決定は、ファイナライザーを実行する1つまたは複数のスレッドとファイナライザーが実行される順序を選択することです。ファイナライザーは、単一のスレッドで順次実行することも、複数のスレッドで同時に実行することもできます。プログラムが、特定の順序で、または特定のスレッドによって実行されるファイナライザーの正確さに何らかの形で依存している場合、一部のJVM実装では機能しますが、他の実装では失敗する可能性があります。

また、Javaは、finalize()メソッドが正常に戻るか、例外をスローして突然完了するかに関係なく、オブジェクトがファイナライズされていると見なすことにも注意してください。ガベージコレクターは、ファイナライザーによってスローされた例外を無視し、例外がスローされたことをアプリケーションの残りの部分に通知することはありません。特定のファイナライザーが特定のミッションを完全に実行することを確認する必要がある場合は、ファイナライザーがミッションを完了する前に発生する可能性のある例外を処理するように、そのファイナライザーを作成する必要があります。

ファイナライザーに関するもう1つの経験則は、アプリケーションの存続期間の終了時にヒープに残されたオブジェクトに関するものです。デフォルトでは、ガベージコレクターは、アプリケーションの終了時にヒープに残っているオブジェクトのファイナライザーを実行しません。このデフォルトを変更runFinalizersOnExit()するには、classRuntimeまたはのメソッドを呼び出して、単一のパラメーターとしてSystem渡す必要がありtrueます。プログラムに、プログラムが終了する前にファイナライザーを絶対に呼び出す必要があるオブジェクトが含まれている場合は、必ずrunFinalizersOnExit()プログラムのどこかで呼び出すようにしてください。

では、ファイナライザーは何に適していますか?

今では、ファイナライザーをあまり使用していないように感じるかもしれません。設計するクラスのほとんどにファイナライザーが含まれていない可能性がありますが、ファイナライザーを使用する理由はいくつかあります。

ファイナライザーの妥当なアプリケーションの1つは、ネイティブメソッドによって割り当てられたメモリを解放することです。オブジェクトがメモリを割り当てるネイティブメソッド(おそらくを呼び出すC関数)を呼び出すmalloc()場合、そのオブジェクトのファイナライザーは、そのメモリを解放する(を呼び出すfree())ネイティブメソッドを呼び出すことができます。この状況では、ファイナライザーを使用して、オブジェクトに代わって割り当てられたメモリ(ガベージコレクターによって自動的に再利用されないメモリ)を解放します。

ファイナライザーのもう1つのより一般的な使用法は、ファイルハンドルやソケットなどの非メモリ有限リソースを解放するためのフォールバックメカニズムを提供することです。前に述べたように、有限の非メモリリソースを解放するためにファイナライザーに依存するべきではありません。代わりに、リソースを解放するメソッドを提供する必要があります。ただし、リソースが既にリリースされていることを確認するファイナライザーを含めることもできます。リリースされていない場合は、先に進んでリリースします。このようなファイナライザーは、クラスの雑な使用を防ぎます(そして、うまくいけば、奨励しないでしょう)。クライアントプログラマーがリソースを解放するために指定したメソッドを呼び出すのを忘れた場合、オブジェクトがガベージコレクションされた場合、ファイナライザーはリソースを解放します。のfinalize()方法LogFileManager この記事の後半で示すクラスは、この種のファイナライザーの例です。

ファイナライザーの乱用を避ける

ファイナライズの存在は、JVMにとっていくつかの興味深い複雑さを生み出し、Javaプログラマーにとっていくつかの興味深い可能性を生み出します。ファイナライズがプログラマーに与えるのは、オブジェクトの生と死に対する力です。つまり、Javaでは、ファイナライザーでオブジェクトを復活させることが可能であり、完全に合法です。つまり、オブジェクトを再度参照することで、オブジェクトを復活させることができます。(ファイナライザーがこれを実現する1つの方法は、ファイナライズされるオブジェクトへの参照を、まだ「ライブ」である静的リンクリストに追加することです。)このようなパワーは、重要だと感じさせるため、行使したくなるかもしれませんが、経験則この力を使いたいという誘惑に抵抗することです。一般に、ファイナライザーでオブジェクトを復活させることは、ファイナライザーの乱用を構成します。

このルールの主な理由は、復活を使用するプログラムはすべて、復活を使用しないわかりやすいプログラムに再設計できるということです。この定理の正式な証明は読者への演習として残されていますが(私はいつもそれを言いたかったのですが)、非公式の精神では、オブジェクトの復活はオブジェクトのファイナライズと同じくらいランダムで予測不可能であると考えてください。そのため、復活を使用する設計は、Javaでのガベージコレクションの特異性を完全に理解していない可能性がある次のメンテナンスプログラマーが理解するのは困難です。

オブジェクトを復活させる必要があると感じた場合は、同じ古いオブジェクトを復活させるのではなく、オブジェクトの新しいコピーを複製することを検討してください。このアドバイスの背後にある理由は、JVMのガベージコレクターがfinalize()オブジェクトのメソッドを1回だけ呼び出すということです。そのオブジェクトが復活し、ガベージコレクションにfinalize()再び使用できるようになった場合、オブジェクトのメソッドは再度呼び出されません。