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ではこれが発生しないので、実際には安心しています。なぜなら、this
とother
は、スコープ外になる前にガベージコレクションを取得するからです。 this
がメソッドが呼び出されるオブジェクトであり、other
がメソッドの引数であることを考えると、これはさらに奇妙です。したがって、両方とも、スコープ内ですでに「生きている」はずです。メソッドが呼び出されています。
これに対する簡単な回避策は、this
とother
の両方でいくつかのダミーメソッドを呼び出すか(醜い!)、ネイティブメソッドに渡すことです(そこで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アプローチを使用しても、実際には危険ではないと私は信じています。
追加する必要のある他の制限はありますか、またはすべての制限が尊重されていてもファイナライザーが安全であることを保証する方法は本当にありませんか?
私自身の見解は、ネイティブオブジェクトを使い終わったらすぐに、決定論的な方法でリリースする必要があるというものです。そのため、スコープを使用してそれらを管理することは、ファイナライザーに依存するよりも望ましい方法です。最後の手段としてファイナライザーを使用してクリーンアップすることもできますが、あなた自身の質問で実際に指摘した理由から、実際の寿命を管理するためだけに使用することはありません。
そのため、ファイナライザーを最後の試みとしますが、最初の試みではありません。
この議論のほとんどは、finalize()のレガシーステータスから生じていると思います。 Javaで導入され、ガベージコレクションでカバーされなかったものに対処しましたが、必ずしもシステムリソース(ファイル、ネットワーク接続など)のようなものではないため、常に中途半端な感じがしました。パターン自体に問題がある場合、finalize()よりも優れたファイナライザーであると公言しているphantomreferenceのようなものを使用することに必ずしも同意しません。
Hugues Morea finalize()はJava 9で非推奨になると指摘しました。Javaチームの推奨パターンはネイティブピアなどをシステムリソースとして扱い、try-with-resourcesを介してクリーンアップします。 AutoCloseable を実装すると、これを実行できます。try-with-resourcesとAutoCloseableは、どちらもJoshBlochの日付より後の日付であることに注意してください。 JavaおよびEffective Java第2版。
finalize
およびオブジェクトのライフタイムに関するGCの知識を使用するその他のアプローチには、いくつかのニュアンスがあります。
ファイナライザーを使用してこれらすべての問題を解決することは可能ですが、かなりの量のコードが必要です。ハンス-J。ベームは 素晴らしいプレゼンテーション これらの問題と可能な解決策を示しています。
visibilityを保証するには、コードを同期する必要があります。つまり、通常のメソッドにReleaseセマンティクスを使用した操作と、Acquireセマンティクスを使用した操作を配置する必要があります。ファイナライザーで。例えば:
volatile
のストア+ファイナライザーで同じvolatile
を読み取ります。keepAlive
実装を参照)スライド)。到達可能性(言語仕様でまだ保証されていない場合)を保証するには、次を使用できます。
Reference#reachabilityFence
from Java 9。nativeMultiply
はstatic
であるため、this
多分ガベージコレクションされます。プレーンなfinalize
とPhantomReferences
の違いは、後者の方がファイナライズのさまざまな側面をより細かく制御できることです。
ReferenceQueues
)。B
がA
からPhantomReference
のフィールドとして確定されたときに存続する必要があるオブジェクトA
への強力な参照を保持します。PhantomRefereces
をGCによってキューに入れられるまで強く到達可能に保つため、安全な終了を実装するのが簡単になります。どうすればこれをガベージコレクションできるでしょうか?
関数nativeMultiply(long xCppPtr, long yCppPtr)
は静的であるため。ネイティブ関数が静的である場合、その2番目のパラメーターはjclass
がjobject
を指すのではなく、そのクラスを指すthis
です。したがって、この場合、this
は引数の1つではありません。
静的でなかった場合は、other
オブジェクトでのみ問題が発生します。
https://github.com/Android/platform_frameworks_base/blob/master/graphics/Java/Android/graphics/Bitmap.Java#L135 ファイナライザーの代わりにphantomreferenceを使用するを参照してください
挑発的な提案を考えさせてください。マネージドJavaオブジェクトのC++側を連続したメモリに割り当てることができる場合は、従来のlongネイティブポインタの代わりにDirectByteBuffer。これは本当にゲームチェンジャーかもしれません。GCはこれらの小さなJava巨大なネイティブデータ構造のラッパー(たとえば、以前に収集することを決定する)について十分に賢くなります。
残念ながら、ほとんどの実際のC++オブジェクトはこのカテゴリに分類されません...