web-dev-qa-db-ja.com

Object.finalize()のオーバーライドは本当に悪いですか?

Object.finalize()のオーバーライドに対する主な2つの引数は次のとおりです。

  1. いつ呼び出されるかを決めることはできません。

  2. まったく呼び出されない場合があります。

これを正しく理解していれば、Object.finalize()をそれほど憎む十分な理由にはならないでしょう。

  1. VM実装とGCが、オブジェクトの割り当てを解除する適切なタイミングがいつであるかを決定するのは開発者ではありません。Object.finalize()がいつ呼び出されるかを決定することが重要なのはなぜですか? ?

  2. 通常、私が間違っている場合は修正してください。Object.finalize()が呼び出されないのは、GCが実行される前にアプリケーションが終了したときだけです。ただし、アプリケーションのプロセスが終了すると、オブジェクトはとにかく割り当て解除されます。したがって、Object.finalize()は呼び出す必要がないため、呼び出されませんでした。開発者が気にするのはなぜですか?

手動で閉じる必要があるオブジェクト(ファイルハンドルや接続など)を使用するたびに、非常にイライラします。オブジェクトにclose()の実装があるかどうかを常に確認する必要があります。また、過去のある時点で、いくつかの呼び出しを逃してしまったことは確かです。 close()実装をObject.finalize()に配置することで、これらのオブジェクトを破棄するためにVMおよびGCに任せたほうが簡単で安全ではないのはなぜですか?

34
AxiomaticNexus

私の経験では、1つだけありますオーバーライドする理由Object.finalize() 、しかしそれはとても良い理由です

エラーログコードをfinalize()に配置して、close()の呼び出しを忘れた場合に通知します。

静的分析は軽微な使用シナリオでのみ省略を捕捉できます。別の回答で言及されているコンパイラの警告には、単純ではないことを行うためにそれらを無効にする必要があるという単純な見方があります。 (私が知っている、または聞いたことのある他のどのプログラマーよりもはるかに多くの警告を有効にしていますが、愚かな警告を有効にしていません。)

ファイナライズは、リソースが破棄されないようにするための優れたメカニズムのように見えるかもしれませんが、ほとんどの人は完全に間違った方法でそれを見ます:彼らは代替フォールバックメカニズム、つまり自動的に彼らが忘れていた資源を処分することで一日。 これは完全に間違っています。特定のことを行う方法は1つだけでなければなりません。常にすべてを閉じるか、ファイナライズによって常にすべてを閉じるかのいずれかです。しかし、ファイナライズは信頼できないので、ファイナライズはそれではありません。

だから、私が強制処分と呼ぶこのスキームがあり、それはプログラマが常にすべてを明示的に閉じる責任があると規定していますCloseableまたはAutoCloseableを実装します。 (try-with-resourcesステートメントは、明示的な終了として引き続きカウントされます。)もちろん、プログラマは忘れる可能性があるため、ファイナライズが行われますが、ではありません魔法の妖精として最後に、魔法のように物事を正しく行います:ファイナライゼーションがclose()が呼び出されなかったことを検出した場合、notがそれを呼び出そうとします。数学的確実性)n00bプログラマーの群れであり、怠惰または不在すぎて実行することができないという仕事をするためにそれに依存するでしょう。したがって、必須の処理で、ファイナライゼーションがclose()が呼び出されなかったことを検出すると、明るい赤のエラーメッセージをログに記録し、太字のすべて大文字のプログラマーに、自分のサービスを修正するように伝えます。

追加の利点として、 噂がある 「JVMは些細なfinalize()メソッドを無視します(たとえば、Objectクラスで定義されているような、何もせずに単に戻るメソッド)」。必須の処分システム全体でのすべてのファイナライズオーバーヘッドを回避このオーバーヘッドがどれほどひどいかについては、Alipの回答を参照finalize()メソッドを次のようにコーディングします。

@Override
protected void finalize() throws Throwable
{
    if( Global.DEBUG && !closed )
    {
        Log.Error( "FORGOT TO CLOSE THIS!" );
    }
    //super.finalize(); see alip's comment on why this should not be invoked.
}

これの背後にある考えはGlobal.DEBUGstatic final変数はコンパイル時に値がわかっているため、falseの場合、コンパイラーはifステートメント全体に対してコードをまったく発行しないため、これはatrivial(空の)ファイナライザ。これは、クラスがファイナライザを持たないかのように扱われることを意味します。 (C#では、これはNice #if DEBUGブロック、しかし何ができるか、これはJavaであり、頭の中でオーバーヘッドを追加することでコードを単純化します。)

ドットネットでのリソースの破棄についての追加の議論とともに、必須の破棄についての詳細は、こちら: michael.gr:必須の破棄と「破棄-破棄」の忌避

45
Mike Nakis

手動で閉じる必要があるオブジェクト(ファイルハンドルや接続など)を使用するたびに、非常にイライラします。 [...] VMおよびGCに任せて、close()実装をObject.finalize()

ファイルハンドルと接続(つまり、LinuxおよびPOSIXシステムでは ファイル記述子 )は非常に少ないリソースであるため(一部のシステムでは256に、その他のシステムでは16384に制限される場合があります。 setrlimit(2) )。このような限られたリソースを使い果たしないように、GCが頻繁に(または適切なタイミングで)呼び出されるという保証はありません。そして、GCが十分に呼び出されない場合(またはファイナライズが適切なタイミングで実行されない場合)は、その(おそらく低い)制限に達します。

ファイナライズは、JVMにおける「最善の努力」です。呼び出されないか、かなり遅く呼び出される可能性があります...特に、RAMがたくさんある場合、またはプログラムが多くのオブジェクトを割り当てない場合(またはそれらはコピー世代のGCによって十分に古い世代に転送される前に終了します)、GCがめったに呼び出されず、ファイナライズが頻繁に実行されない可能性があります(またはまったく実行されない場合もあります)。

したがって、可能であれば、closeファイル記述子を明示的に指定します。それらがリークすることを恐れている場合は、ファイナライズを主要なものとしてではなく、追加の手段として使用してください。

28

問題をこのように見てください:(a)正しい(そうでなければプログラムが明らかに間違っている)と(b)必要な(そうでなければコードが大きすぎる、つまりRAM必要、役に立たないものに費やされるより多くのサイクル、それを理解するためのより多くの努力、それを維持するために費やされるより多くの時間など)

次に、ファイナライザで何をしたいかを考えます。 どちらか必要です。その場合、ファイナライザーに入れることはできません。呼び出されるかどうかがわからないためです。それは十分ではありません。 またはそれは必要ではありません-そもそもそれを書くべきではありません!どちらにしても、ファイナライザに入れるのは間違った選択です。

(ファイルストリームを閉じるなど、名前を付ける例はlook本当に必要ではないかのように見えますが、実際には必要です。システムで開いているファイルハンドルの制限に達しても、コードが正しくないことは通知しませんが、その制限はオペレーティングシステムの機能ですしたがって、ファイナライザに関するJVMのポリシーよりも予測不可能であるため、ファイルハンドルを無駄にしないことが本当にis重要です。)

13
Kilian Foth

ファイナライザに依存しない最大の理由の1つは、ファイナライザでクリーンアップしようとするリソースのほとんどが非常に限られていることです。ガベージコレクターは時々しか実行されません。何かを解放できるかどうかを判断するために参照をトラバースするのはコストがかかるためです。これは、オブジェクトが実際に破棄されるまでに「しばらく」かかる可能性があることを意味します。たとえば、短期間のデータベース接続を開くオブジェクトがたくさんある場合、ファイナライザがこれらの接続をクリーンアップするままにしておくと、ガベージコレクタが最終的に実行されて終了した接続を解放するのを待つ間、接続プールが使い果たされる可能性があります。次に、待機のために、キューに入れられた要求の大量のバックログが発生し、接続プールがすぐに使い果たされます。これは、アプリケーションをすぐに動作不能状態にするサイクルです。

さらに、try-with-resourcesを使用すると、完了時に「クローズ可能な」オブジェクトを閉じるのが簡単になります。この構成に慣れていない場合は、チェックアウトすることをお勧めします。 https://docs.Oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

8
Dogs

リソースを解放するためにファイナライザに任せることが一般に悪い考えであることに加えて、ファイナライズ可能なオブジェクトにはパフォーマンスのオーバーヘッドが伴います。

Javaの理論と実践:ガベージコレクションとパフォーマンス(Brian Goetz)から ファイナライザーはあなたの友達ではありません

ファイナライザを備えたオブジェクト(重要でないfinalize()メソッドを持つもの)は、ファイナライザを持たないオブジェクトと比較してオーバーヘッドが大きいため、慎重に使用する必要があります。ファイナライズ可能なオブジェクトは、割り当てと収集の両方に時間がかかります。割り当て時に、JVMはファイナライズ可能なオブジェクトをガベージコレクターに登録する必要があり、(少なくともHotSpot JVM実装では)ファイナライズ可能なオブジェクトは他のほとんどのオブジェクトよりも遅い割り当てパスに従う必要があります。同様に、ファイナライズ可能なオブジェクトも収集に時間がかかります。ファイナライズ可能なオブジェクトを再利用できるようになるまでに、少なくとも2つのガベージコレクションサイクル(最良の場合)が必要であり、ガベージコレクタはファイナライザを呼び出すために追加の作業を行う必要があります。その結果、到達不能なファイナライズ可能なオブジェクトによって使用されるメモリがより長く保持されるため、オブジェクトの割り当てと収集により多くの時間が費やされ、ガベージコレクタへの圧力が高くなります。これとファイナライザーが予測可能な時間枠での実行が保証されていない、またはまったく保証されていないという事実を組み合わせると、ファイナライズが適切なツールである状況が比較的少ないことがわかります。

8
alip

私の(最も少ない)お気に入りの理由Object.finalizeは、オブジェクトが期待した後にファイナライズされる可能性があるわけではありませんが、期待される前にファイナライズされる可能性があります。 Javaが到達不可能と判断した場合、スコープが終了する前に、まだスコープ内にあるオブジェクトをファイナライズできるという問題はありません。

void test() {
   HasFinalize myObject = ...;
   OutputStream os = myObject.stream;

   // myObject is no-longer reachable at this point, 
   // even though it is in scope. But objects are finalized
   // based on reachability.
   // And since finalization is on another thread, it 
   // could happen before or in the middle of the write .. 
   // closing the stream and causing much fun.
   os.write("Hello World");
}

詳細は この質問 を参照してください。さらに楽しいのは、この決定はホットスポットの最適化が有効になって初めて行われるため、デバッグが困難になることです。

7

オブジェクトにclose()の実装があるかどうかを常に確認する必要があり、過去のいくつかの時点で、そのオブジェクトへの呼び出しをいくつか見逃していると確信しています。

Eclipseでは、Closeable/AutoCloseableを実装するものを閉じるのを忘れると警告が表示されます。それがEclipseの問題なのか、それとも公式コンパイラーの一部なのかはわかりませんが、同様の静的分析ツールを使用してそこを支援することを検討するかもしれません。たとえば、FindBugsは、リソースを閉じるのを忘れたかどうかを確認するのに役立つでしょう。

4
Nebu Pookins

最初の質問へ:

VMの実装とGCが、オブジェクトの割り当てを解除する適切なタイミングがいつであるかを判断するのは、開発者ではありません。 Object.finalize()がいつ呼び出されるかを決定することが重要なのはなぜですか?

まあ、JVMは、オブジェクトに割り当てられたストレージを再利用するのが適切なタイミングを判断します。これは、finalize()で実行したいリソースのクリーンアップが発生するタイミングであるとは限りません。これは、SOの 「finalize()がJava 8の強力な到達可能オブジェクトで呼び出される」 質問に示されています。そこで、close()メソッドがfinalize()メソッドによって呼び出されましたが、同じオブジェクトによるストリームからの読み取りの試行はまだ保留中です。したがって、finalize()が遅れて呼び出されるというよく知られた可能性に加えて、早すぎる可能性があります。

2番目の質問の前提:

通常、私が間違っている場合は修正してください。Object.finalize()が呼び出されないのは、GCが実行される前にアプリケーションが終了したときだけです。

単に間違っています。 JVMがファイナライズをサポートする必要はまったくありません。まあ、アプリケーションが終了することを前提として、「ファイナライズが行われる前にアプリケーションが終了した」と解釈する可能性があるため、完全に間違っているわけではありません。

ただし、元のステートメントの「GC」と「ファイナライズ」という用語の小さな違いに注意してください。ガベージコレクションはファイナライズとは異なります。オブジェクトが到達不能であることをメモリ管理が検出すると、特別なfinalize()メソッドがないか、ファイナライズが単純にサポートされていないか、ファイナライズのためにオブジェクトをキューに入れる場合、その領域を再利用します。 。したがって、ガベージコレクションサイクルの完了は、ファイナライザが実行されることを意味しません。これは後で、キューが処理されるときに発生するか、まったく発生しない可能性があります。

この点は、ファイナライズサポートを備えたJVMであっても、リソースのクリーンアップに依存することが危険である理由でもあります。ガベージコレクションはメモリ管理の一部であるため、メモリのニーズによってトリガーされます。ランタイム全体で十分なメモリがあるため、ガベージコレクションが実行されない可能性があります(それでも、「GCが実行される前にアプリケーションが終了した」という説明に適合します)。 GCは実行される可能性もありますが、その後、十分なメモリが解放されるため、ファイナライザキューは処理されません。

言い換えると、この方法で管理されたネイティブリソースは、依然としてメモリ管理の対象外です。 OutOfMemoryErrorは、メモリを解放するための十分な試行の後にのみスローされることが保証されていますが、ネイティブリソースおよびファイナライズには適用されません。ファイナライザがこれまでに実行された場合、ファイナライゼーションキューがこれらのリソースを解放する可能性のあるオブジェクトでいっぱいであるときに、リソース不足のためにファイルを開くことができない可能性があります…

2
Holger