web-dev-qa-db-ja.com

Javaファイナライザーはネイティブピアオブジェクトのライフサイクル管理でも本当に避けるべきですか?

C++/Java/Android開発者としての私の経験では、ファイナライザーはほとんどの場合悪い考えであることがわかりました。唯一の例外は、Javaが必要とする「ネイティブピア」オブジェクトの管理です。 JNIを介してC/C++コードを呼び出す。

JNI:Javaオブジェクトの質問の存続期間を適切に管理する を知っていますが、この質問は、ファイナライザーを使用しない理由に対処しますとにかく、ネイティブピアの場合も。それで、それは前述の質問の答えの論争についての質問/議論です。

JoshuaBlochの Effective Java は、ファイナライザーを使用しないことに関する彼の有名なアドバイスの例外として、このケースを明示的にリストしています。

ファイナライザーの2番目の正当な使用は、ネイティブピアを持つオブジェクトに関係します。ネイティブピアは、通常のオブジェクトがネイティブメソッドを介して委任するネイティブオブジェクトです。ネイティブピアは通常のオブジェクトではないため、ガベージコレクターはそれを認識せず、Javaピアが再利用されたときにそれを再利用できません。ネイティブピアが重要なリソースを保持していない場合、ファイナライザーはこのタスクを実行するための適切な手段です。ネイティブピアが、迅速に終了する必要のあるリソースを保持している場合、上記のように、クラスには明示的な終了メソッドが必要です。終了メソッドは、重要なリソースを解放するために必要なことは何でも実行する必要があります。終了メソッドは、ネイティブメソッドにすることも、呼び出すこともできます。

"完成したメソッドがJavaに含まれているのはなぜですか?" stackexchangeの質問も参照してください)

それから私は本当に興味深い Androidでネイティブメモリを管理する方法 Google I/O '17での講演を見ました。そこでは、ハンスベームがファイナライザーを使用してネイティブを管理することに対して実際に提唱しています。 Javaオブジェクトのピア。また、Effective Javaを参照として引用しています。ネイティブピアの明示的な削除またはスコープに基づく自動クローズが実行可能な代替手段ではない理由を簡単に述べた後、彼は代わりに_Java.lang.ref.PhantomReference_を使用するようアドバイスします。

彼はいくつかの興味深い点を述べていますが、私は完全には確信していません。誰かがそれらにさらに光を当てることができることを願って、私はそれらのいくつかを実行し、私の疑問を述べようとします。

この例から始めて:

_class BinaryPoly {

    long mNativeHandle; // holds a c++ raw pointer

    private BinaryPoly(long nativeHandle) {
        mNativeHandle = nativeHandle;
    }

    private static native long nativeMultiply(long xCppPtr, long yCppPtr);

    BinaryPoly multiply(BinaryPoly other) {
        return new BinaryPoly ( nativeMultiply(mNativeHandle, other.mNativeHandler) );
    }

    // …

    static native void nativeDelete (long cppPtr);

    protected void finalize() {
        nativeDelete(mNativeHandle);
    }
}
_

Javaクラスが、ファイナライザーメソッドで削除されるネイティブピアへの参照を保持している場合、Blochはそのようなアプローチの欠点をリストします。

ファイナライザーは任意の順序で実行できます

2つのオブジェクトが到達不能になった場合、ファイナライザーは実際には任意の順序で実行されます。これには、相互にポイントする2つのオブジェクトが同時に到達不能になった場合も含まれ、間違った順序でファイナライズされる可能性があります。つまり、2番目のオブジェクトが実際にファイナライズされます。すでにファイナライズされているオブジェクトにアクセスしようとします。 [...]その結果、ダングリングポインターを取得し、割り当て解除されたc ++オブジェクトを確認できます[...]

そして例として:

_class SomeClass {
    BinaryPoly mMyBinaryPoly:
    …
    // DEFINITELY DON’T DO THIS WITH CURRENT BinaryPoly!
    protected void finalize() {
        Log.v(“BPC”, “Dropped + … + myBinaryPoly.toString());   
    }
}
_

わかりましたが、myBinaryPolyが純粋なJavaオブジェクトである場合も、これは当てはまりませんか?私が理解しているように、問題は、所有者のファイナライザー内でファイナライズされた可能性のあるオブジェクトを操作することから発生します。オブジェクトのファイナライザーを使用して独自のプライベートネイティブピアを削除するだけで、他に何もしない場合は、問題ないはずです。

ファイナライザーは、ネイティブメソッドが実行されるまで呼び出される場合があります

Javaルールによるが、現在Androidではない:
オブジェクトxのファイナライザーは、xのメソッドの1つがまだ実行されていて、ネイティブオブジェクトにアクセスしているときに呼び出される場合があります。

これを説明するために、multiply()がコンパイルされる対象の擬似コードが示されています。

_BinaryPoly multiply(BinaryPoly other) {
    long tmpx = this.mNativeHandle; // last use of “this”
    long tmpy = other.mNativeHandle; // last use of other
    BinaryPoly result = new BinaryPoly();
    // GC happens here. “this” and “other” can be reclaimed and finalized.
    // tmpx and tmpy are still neeed. But finalizer can delete tmpx and tmpy here!
    result.mNativeHandle = nativeMultiply(tmpx, tmpy)
    return result;
}
_

これは恐ろしいことです。Androidではこれが発生しないので、実際には安心しています。なぜなら、thisotherは、スコープ外になる前にガベージコレクションを取得するからです。 thisがメソッドが呼び出されるオブジェクトであり、otherがメソッドの引数であることを考えると、これはさらに奇妙です。したがって、両方とも、スコープ内ですでに「生きている」はずです。メソッドが呼び出されています。

これに対する簡単な回避策は、thisotherの両方でいくつかのダミーメソッドを呼び出すか(醜い!)、ネイティブメソッドに渡すことです(そこでmNativeHandleそしてそれを操作します)。そして待ってください...thisはすでにデフォルトでネイティブメソッドの引数の1つです!

_JNIEXPORT void JNICALL Java_package_BinaryPoly_multiply
(JNIEnv* env, jobject thiz, jlong xPtr, jlong yPtr) {}
_

thisをガベージコレクションするにはどうすればよいですか?

ファイナライザーの延期が長すぎる可能性があります

「これが正しく機能するために、多くのネイティブメモリと比較的少ないJavaメモリを割り当てるアプリケーションを実行する場合、ガベージコレクタが実際にファイナライザを呼び出すのに十分な速さで実行されるとは限りません[.. 。]したがって、実際にはSystem.gc()とSystem.runFinalization()をときどき呼び出さなければならない場合がありますが、これを行うのは難しいです[...]」

ネイティブピアが関連付けられている単一のJavaオブジェクトによってのみ表示される場合、この事実はシステムの他の部分に対して透過的ではないため、GCはライフサイクルを管理する必要があります。 Javaオブジェクトは純粋なJavaオブジェクトでしたか?ここには明らかに私が見落としているものがあります。

ファイナライザーは実際にJavaオブジェクトの寿命を延ばすことができます

[...]ファイナライザーは、実際にJavaオブジェクトの存続期間を別のガベージコレクションサイクルに延長することがあります。つまり、世代別のガベージコレクターの場合、実際に古い世代まで存続させ、存続期間が大幅に長くなる可能性があります。ファイナライザーを持っているだけの結果として拡張されました。

私はここで何が問題であり、それがネイティブピアを持つことにどのように関連しているかを実際には理解していないことを認めます、私はいくつかの調査を行い、おそらく質問を更新します:)

結論

今のところ、ネイティブピアがJavaオブジェクトのコンストラクターで作成され、finalizeメソッドで削除された場合、一種のRAIIアプローチを使用しても、実際には危険ではないと私は信じています。

  • ネイティブピアは重要なリソースを保持していません(その場合、リソースを解放するための別のメソッドが必要です。ネイティブピアは、ネイティブレルムのJavaオブジェクト「カウンターパート」としてのみ機能する必要があります)
  • ネイティブピアはスレッドにまたがったり、デストラクタで奇妙な並行処理を実行したりしません(誰がそれを実行したいですか?!?)
  • ネイティブピアポインタは、Javaオブジェクトの外部で共有されることはなく、単一のインスタンスにのみ属し、Javaオブジェクトのメソッドの内部でのみアクセスされます。 Androidでは、Javaオブジェクトは、異なるネイティブピアを受け入れるjniメソッドを呼び出す直前、またはJavaオブジェクトを渡すだけで、同じクラスの別のインスタンスのネイティブピアにアクセスできます。ネイティブメソッド自体に
  • Javaオブジェクトのファイナライザーは、それ自体のネイティブピアのみを削除し、他には何もしません。

追加する必要のある他の制限はありますか、またはすべての制限が尊重されていてもファイナライザーが安全であることを保証する方法は本当にありませんか?

37
athos

私自身の見解は、ネイティブオブジェクトを使い終わったらすぐに、決定論的な方法でリリースする必要があるというものです。そのため、スコープを使用してそれらを管理することは、ファイナライザーに依存するよりも望ましい方法です。最後の手段としてファイナライザーを使用してクリーンアップすることもできますが、あなた自身の質問で実際に指摘した理由から、実際の寿命を管理するためだけに使用することはありません。

そのため、ファイナライザーを最後の試みとしますが、最初の試みではありません。

5
cineam mispelt

この議論のほとんどは、finalize()のレガシーステータスから生じていると思います。 Javaで導入され、ガベージコレクションでカバーされなかったものに対処しましたが、必ずしもシステムリソース(ファイル、ネットワーク接続など)のようなものではないため、常に中途半端な感じがしました。パターン自体に問題がある場合、finalize()よりも優れたファイナライザーであると公言しているphantomreferenceのようなものを使用することに必ずしも同意しません。

Hugues Morea finalize()はJava 9で非推奨になると指摘しました。Javaチームの推奨パターンはネイティブピアなどをシステムリソースとして扱い、try-with-resourcesを介してクリーンアップします。 AutoCloseable を実装すると、これを実行できます。try-with-resourcesとAutoCloseableは、どちらもJoshBlochの日付より後の日付であることに注意してください。 JavaおよびEffective Java第2版。

4
Scott

finalizeおよびオブジェクトのライフタイムに関するGCの知識を使用するその他のアプローチには、いくつかのニュアンスがあります。

  • visibility:作成されたオブジェクトoのすべての書き込みメソッドがファイナライザーに表示されることを保証しますか(つまり、 happens-beforeオブジェクトに対する最後のアクションの関係oとファイナライズを実行するコード)?
  • 到達可能性:オブジェクトoが時期尚早に破壊されないことをどのように保証しますか(たとえば、そのメソッドの1つが実行中)、JLSで許可されていますか? doeshappen そしてクラッシュを引き起こします。
  • ordering:オブジェクトがファイナライズされる特定の順序を強制できますか?
  • termination:アプリが終了したときにすべてのオブジェクトを破棄する必要がありますか?

ファイナライザーを使用してこれらすべての問題を解決することは可能ですが、かなりの量のコードが必要です。ハンス-J。ベームは 素晴らしいプレゼンテーション これらの問題と可能な解決策を示しています。

visibilityを保証するには、コードを同期する必要があります。つまり、通常のメソッドにReleaseセマンティクスを使用した操作と、Acquireセマンティクスを使用した操作を配置する必要があります。ファイナライザーで。例えば:

  • 各メソッドの最後にあるvolatileのストア+ファイナライザーで同じvolatileを読み取ります。
  • 各メソッドの最後にオブジェクトのロックを解除します+ファイナライザーの最初にロックを取得します(BoehmのkeepAlive実装を参照)スライド)。

到達可能性(言語仕様でまだ保証されていない場合)を保証するには、次を使用できます。

プレーンなfinalizePhantomReferencesの違いは、後者の方がファイナライズのさまざまな側面をより細かく制御できることです。

  • ファントム参照を受信する複数のキューで、それぞれのファイナライズを実行するスレッドを選択できます。
  • 割り当てを行ったのと同じスレッドでファイナライズできます(例:スレッドローカルReferenceQueues)。
  • 順序付けの実施が容易:BAからPhantomReferenceのフィールドとして確定されたときに存続する必要があるオブジェクトAへの強力な参照を保持します。
  • mustPhantomReferecesをGCによってキューに入れられるまで強く到達可能に保つため、安全な終了を実装するのが簡単になります。
4
Dmitry Timofeev

どうすればこれをガベージコレクションできるでしょうか?

関数nativeMultiply(long xCppPtr, long yCppPtr)は静的であるため。ネイティブ関数が静的である場合、その2番目のパラメーターはjclassjobjectを指すのではなく、そのクラスを指すthisです。したがって、この場合、thisは引数の1つではありません。

静的でなかった場合は、otherオブジェクトでのみ問題が発生します。

1
ferini

https://github.com/Android/platform_frameworks_base/blob/master/graphics/Java/Android/graphics/Bitmap.Java#L135 ファイナライザーの代わりにphantomreferenceを使用するを参照してください

1
wanpen

挑発的な提案を考えさせてください。マネージドJavaオブジェクトのC++側を連続したメモリに割り当てることができる場合は、従来のlongネイティブポインタの代わりにDirectByteBuffer。これは本当にゲームチェンジャーかもしれません。GCはこれらの小さなJava巨大なネイティブデータ構造のラッパー(たとえば、以前に収集することを決定する)について十分に賢くなります。

残念ながら、ほとんどの実際のC++オブジェクトはこのカテゴリに分類されません...

0
Alex Cohn