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


34

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

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

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

私がこれを正しく理解していれば、Object.finalize()それがそんなに嫌いな十分な理由だとは思いません。

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

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

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


1
また、Java 1.0時代の多くのAPIと同様に、スレッドのセマンティクスfinalize()は少し台無しになっています。実装する場合は、同じオブジェクトの他のすべてのメソッドに対してスレッドセーフであることを確認してください。
-billc.cn

2
ファイナライザが悪いという人の声を聞いても、それらがあればプログラムが機能しなくなるわけではありません。ファイナライズのアイデア全体がまったく役に立たないことを意味します。
user253751

1
この質問に+1。以下の回答のほとんどは、ファイル記述子などのリソースが制限されており、手動で収集する必要があると述べています。メモリについても同じことが当てはまります。したがって、メモリ収集の遅延を受け入れた場合、ファイル記述子やその他のリソースで遅延を受け入れないのはなぜですか?
mbonnin

最後の段落を取り上げると、Javaに任せて、ファイルハンドルや接続などの終了処理をほとんど手間をかけずに処理できます。try-with-resourcesブロックを使用してください-回答とコメントで既にいくつか言及されていますが、ここに置く価値があると思います。このためのOracleチュートリアルは、docs.oracle.com
javase /

回答:


45

私の経験では、 、オーバーライドする唯一の理由がありますObject.finalize()が、それは非常に良い理由です。

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

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

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

ですから、私はMandatory Disposalと呼んでいるこのスキームがあり、プログラマーは、またはを実装するすべて常に明示的に閉じる責任があると規定しています。(try-with-resourcesステートメントは依然として明示的な終了としてカウントされます。)もちろん、プログラマーは忘れることがあります。そのため、ファイナライズが必要になりますが、魔法の妖精としてではありません。それは呼び出されなかった、それはしませCloseableAutoCloseableclose()を呼び出します。怠けすぎていたり、気が抜けすぎていたりする仕事をするためにそれに依存するプログラマー。したがって、必須の処分では、ファイナライズがそれを発見したときclose()が呼び出されなかったことを検出すると、明るい赤色のエラーメッセージをログに記録し、プログラマーにすべての大文字を太字で伝えて、自分のものを修正します。

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

@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.DEBUGは、static finalコンパイル時に値がわかっている変数であるためfalse、コンパイラーはifステートメント全体に対してコードをまったく出力しないため、これは些細な(空の)ファイナライザーになります。クラスがファイナライザーを持たないかのように扱われることを意味します。(C#では、これは素晴らしい#if DEBUGブロックでが、何ができるのか、これはjavaです。ここでは、コードの見た目の単純さを頭に追加のオーバーヘッドを加えて支払います。)

強制廃棄についての詳細は、ドットネットでのリソースの廃棄についての追加の議論とともに、こちら:michael.gr:強制廃棄と「廃棄処分」の憎悪


2
:@MikeNakisは、開閉可能な第二回呼び出された場合は何もしないと定義され、忘れてはいけないdocs.oracle.com/javase/7/docs/api/java/io/Closeable.htmlを。クラスを2回閉じたときに警告を記録することもありますが、技術的にはそれを行うことすら想定されていません。技術的には、Closableで.close()を複数回呼び出すことは完全に有効です。
パトリックM

1
@usrは、テストを信頼するか、テストを信頼しないかで決まります。テストを信頼できない場合は、念のため、ファイナライズのオーバーヘッド close()発生します。私のテストが信頼されない場合、システムを実稼働環境にリリースしない方が良いと考えています。
マイクナキス

3
@Mikeはif( Global.DEBUG && ...、JVMがfinalize()メソッドを些細なものとして無視するように機能するために、コンストラクトGlobal.DEBUG時に(挿入などとは対照的に)設定する必要があるため、次のコードはデッドコードになります。ifクラスのsuper.finalize()外部への呼び出しGlobal.DEBUGも、スーパークラス#finalize()が些細なものであったとしても、JVMがそれを(少なくともHotSpot 1.8ではの値に関係なく)非自明として扱うのに十分です!
alip

1
@マイク私はそれが事実だと思う。リンクした記事のテストの(わずかに修正したバージョン)でテストし、冗長なGC出力(驚くほどパフォーマンスが低い)を使用して、オブジェクトがサバイバー/古い生成スペースにコピーされ、取得するために完全なヒープGCが必要であることを確認しました取り除く。
-alip

1
しばしば見落とされがちなのは、ファイナライザのリソースを解放することを非常に危険なアクションにする、予想よりも早いオブジェクトのコレクションのリスクです。Java 9より前では、ファイナライザーがリソースの使用中にリソースを閉じないようにする唯一の方法は、リソースを使用するファイナライザーとメソッドの両方でオブジェクトを同期することです。それがで動作する理由java.ioです。その種のスレッドセーフがウィッシュリストになかった場合、それによって引き起こされるオーバーヘッドが増加しfinalize()ます…
Holger

28

手動で閉じる必要のあるオブジェクト(ファイルハンドルや接続など)を使用するたびに、非常にイライラします。[...] close()実装を入れることでこれらのオブジェクトを破棄するためにVMとGCに任せる方が単純で安全ではないのはなぜObject.finalize()ですか?

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

ファイナライズは、JVMの「ベストエフォート」です。呼び出されないか、かなり遅く呼び出される可能性があります...特に、RAMが大量にある場合、またはプログラムが大量のオブジェクトを割り当てない場合(または、それらのほとんどが、古いものに転送される前に死ぬ場合)世代GCのコピーによる生成)、GCは非常にまれに呼び出される可能性があり、ファイナライズは非常に頻繁に実行されない可能性があります(まったく実行されない場合もあります)。

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


7
ファイルまたはソケットにストリームを閉じると、通常はフラッシュされることを追加します。ストリームを不必要に開いたままにすると、電源が切れたり、接続が切断されたり(これはネットワーク経由でアクセスされるファイルのリスクでもある)場合のデータ損失のリスクが増加します。

2
実際、許可されるファイル記述子の数は少ないかもしれませんが、少なくともGCのシグナルとして使用できるため、それは本当に難しい点でありません。本当に問題なのは、a)GCに対して完全に透過的で、GCで管理されていないリソースがどれだけ残っているか、b)それらのリソースの多くが一意であるため、他のリソースがブロックまたは拒否される可能性があることです。
デデュプリケーター

2
また、ファイルを開いたままにすると、他の誰かがそれを使用するのを妨げる可能性があります。(Windows 8 XPSビューアー、私はあなたを見ています!)
ローレンペクテル

2
「[ファイル記述子]のリークを恐れている場合は、主要な手段としてではなく、追加の手段としてファイナライズを使用してください。」この声明は私には怪しげに聞こえます。コードをうまく設計したら、クリーンアップを複数の場所に広げる冗長コードを本当に導入すべきでしょうか?
ムカホ

2
@BasileStarynkevitch理想の世界では、そうです、冗長性は悪いですが、実際には、すべての関連する側面を予見できない、実際には後悔するより安全であるという点ですか?
ムカホ

13

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

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

(注名前の例それは、閉じたファイルのように、ストリームを見て、彼らが本当に必要じゃないかのように、彼らはある。それはあなたのシステム上で開いているファイルハンドルの制限をヒットするまで、あなたがいないということだけだ通告あなたのことただし、この制限はオペレーティングシステムの機能であるため、ファイナライザーに対するJVMのポリシーよりもさらに予測が難しいため、ファイルハンドルを無駄にしないこと重要です。)


「必要な」コードだけを書くことになっている場合、すべてのGUIアプリケーションですべてのスタイル設定を避けるべきですか?確かにそれは必要ありません。GUIはスタイルを設定しなくても正常に機能し、ひどく見えます。ファイナライザについては、何かを行う必要がある場合がありますが、ファイナライザはオブジェクトGCの間に呼び出されるため、ファイナライザに入れることは可能です。リソースを閉じる必要がある場合、特にガベージコレクションの準備ができている場合は、ファイナライザが最適です。プログラムがリソースの解放を終了するか、ファイナライザが呼び出されます。
クルー

RAMが重い、作成に費用がかかる、閉じることができるオブジェクトがあり、必要のないときにクリアできるように弱く参照するfinalize()ことにした場合、gcサイクルが本当に解放する必要がある場合にそれを閉じるために使用できますRAM。そうでなければ、オブジェクトを使用するたびに再生成して閉じるのではなく、オブジェクトをRAMに保持します。もちろん、開いているリソースは、オブジェクトがGCされるまで解放されませんが、いつでも解放されますが、特定の時間にリソースが解放されることを保証する必要はありません。
クルー

8

ファイナライザに依存しない最大の理由の1つは、ファイナライザでクリーンアップしたいリソースのほとんどが非常に限られていることです。ガベージコレクターは、頻繁に実行されるだけです。これは、何かをリリースできるかどうかを判断するために参照を走査するのは高価だからです。これは、オブジェクトが実際に破棄される前に「しばらく」なる可能性があることを意味します。たとえば、存続期間の短いデータベース接続を開くオブジェクトが多数ある場合、ファイナライザーを残してこれらの接続をクリーンアップすると、ガベージコレクターが最終的に実行されて完了した接続を解放するまで接続プールを使い果たす可能性があります。次に、待機のため、キューに入れられたリクエストの大量のバックログが発生し、接続プールがすぐに使い果たされます。それ'

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


8

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

Javaの理論と実践:ガベージコレクションとパフォーマンス(ブライアン・ゲッツ)、ファイナライザはあなたの友達ではありません

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


素晴らしい点。のパフォーマンスオーバーヘッドを回避する手段を提供する私の回答を参照しくださいfinalize()
マイクナキス

7

私の(最も)避けたい理由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");
}

詳細については、この質問を参照してください。さらに楽しいのは、ホットスポットの最適化が開始された後にのみこの決定が行われる可能性があることであり、これはデバッグに苦痛を与えます。


1
問題は、HasFinalize.streamそれ自体を個別にファイナライズ可能なオブジェクトにする必要があるということです。つまり、のファイナライズはファイナライズHasFinalizeしたり、クリーンアップしようとしたりするべきではありませんstream。または、必要に応じて、streamアクセスできなくする必要があります。
acelent


4

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

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


1
言及することをお勧めしAutoCloseableます。try-with-resourcesでリソースを管理するのが非常に簡単になります。これにより、質問のいくつかの引数が無効になります。

2

最初の質問:

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

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

2番目の質問の前提:

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

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

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.