Java には、強参照、弱参照、弱参照、仮想参照の合計 4 つの参照型があります (実際には、FinalReference などの他の参照型もあります)。
その中で強い参照は私たちがよく使う Object a = new Object(); の形で、Javaには対応する Reference クラスがありません。
この記事では主に、ソフト参照、弱参照、および仮想参照の実装を分析します. これら 3 つの参照型は Reference クラスから継承され、主なロジックも Reference にあります.
質問
分析の前に、いくつか質問をしますか?
1. インターネット上のほとんどの記事でソフト参照が導入されているのは、メモリが不足すると再利用されるというものです. メモリ不足はどのように定義されますか? 何がメモリ不足ですか?
2. インターネット上のほとんどの記事での仮想参照の導入: 仮想参照はオブジェクトのライフサイクルを決定しません。主に、ガベージ コレクターによって回収されるオブジェクトのアクティビティを追跡するために使用されます。本当か?
3. Jdk で仮想参照が使用されるのはどのシナリオですか?
参照
まず、Reference.java のいくつかのフィールドを見てみましょう。
public abstract class Reference { //参照先のオブジェクトprivate T referent; //リファレンス volatile ReferenceQueue<? super T>のコンストラクタで、リサイクル キューをユーザーが指定します。 field はキュー内の次の要素に設定され、リンクされたリスト構造を形成しますvolatile Reference next; //GC の間、JVM の最下層は、参照オブジェクトを格納する DiscoveredList と呼ばれるリンクされたリストと、検出されたフィールド ポイントを維持します。リンクされたリストへ JVM の次の要素が一時的に設定されます private 参照が検出されました; //スレッド同期用のロック オブジェクトstatic private class Lock { } private static Lock lock = new Lock(); //追加されるのを待っている参照オブジェクトキューに、GC JVM 設定中に GC によって設定され、保留中から要素を継続的に抽出し、それらをキューに追加する Java 層スレッド (ReferenceHandler) が存在しますprivate static Reference pending = null; }
Reference オブジェクトのライフサイクルは次のとおりです。
主に、ネイティブ層と Java 層の 2 つの部分に分けられます。
ネイティブ層は、GC 中に再利用する必要がある参照オブジェクトを DiscoveredList に追加し (コードは referenceProcessor.cpp の process_discovered_references メソッドにあります)、DiscoveredList の要素を PendingList に移動します (コードは enqueue_discovered_ref_helper メソッドにあります)。 1 つ目は、Reference クラスの保留中のオブジェクトです。
Java 層のコードを見てください。
private static class ReferenceHandler extends Thread { … public void run() { while (true) { tryHandlePending(true); } } } static boolean tryHandlePending(boolean waitForNotify) { Reference r; Cleaner c; try { synchronized (lock) { if (pending != null) { r = pending; //Cleaner オブジェクトの場合は記録し、以下の特別な処理を行うc = r instanceof Cleaner ? (Cleaner) r : null; //ポイントPendingList の下部にオブジェクトpending = r.discovered; r.discovered = null; } else { //pending が null の場合は、まず待機 PendingList にオブジェクトが追加されると、jvm は通知を実行しますif (waitForNotify) {
lock.wait();
}
// 待機中の場合は再試行
return waitForNotify;
}
}
}
…
// 如果时CLeaner对象,则调用clean方法进行资源回收
if (c != null) {
c.clean();
return true;
}
//将Reference加入到ReferenceQueue,开发者可以通过从ReferenceQueue中poll元素感知到对象被回收的事件。
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
プロセスは比較的単純です: PendingList
から要素を継続的に抽出し、それらを ReferenceQueue に追加することです. 開発者は、ReferenceQueue の poll 要素を介して、オブジェクトがリサイクルされているイベントを認識することができます.
また、タイプ Cleaner (仮想参照から継承) のオブジェクトには追加の処理が行われることにも注意してください:オブジェクトが指すオブジェクトがリサイクルされると、 clean メソッドが呼び出されます. このメソッドは主に、対応するリソースのリサイクルに使用されます.オフヒープ メモリ DirectByteBuffer は Cleaner を使用してオフヒープ メモリをリサイクルします。これは、Java での仮想参照の典型的なアプリケーションでもあります。
Reference の実装を読んだ後、いくつかの実装クラスの違いを見てみましょう。
ソフトリファレンス
public class SoftReference extends Reference {
static private long clock;
private long timestamp;
public SoftReference(T referent) {
super(referent);
this.timestamp = clock;
}
public SoftReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
this.timestamp = clock;
}
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
}
ソフト参照の実装は非常に単純で、さらに 2 つのフィールド (クロックとタイムスタンプ) があります。clock は静的変数であり、このフィールドは GC が発生するたびに現在の時刻に設定されます。タイムスタンプ フィールドは、get メソッドが呼び出されるたびに clock に割り当てられます (等しくなく、オブジェクトが収集されない場合)。
**これら 2 つのフィールドの役割は何ですか? **これは、メモリが十分でない場合に再利用されるソフト参照と何の関係がありますか?
オブジェクトを再利用する必要があるかどうかの決定は GC で実装されるため、これらは JVM のソース コードも確認する必要があります。
size_t
ReferenceProcessor::process_discovered_reflist(
DiscoveredList refs_lists[],
ReferencePolicy* policy,
bool clear_referent,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc,
AbstractRefProcTaskExecutor* task_executor)
{ … //上記の DiscoveredList を覚えていますか? refs_lists は DiscoveredList です。//DiscoveredList の処理はいくつかの段階に分けられ、SoftReference の処理は最初の段階にある... for (uint i = 0; i < _max_num_q; i++) { process_phase1(refs_lists[i], policy, is_alive, keep_alive, complete_gc) ; } … }
//このステージの主な目的は、メモリが十分にあるときに、対応する SoftReference を refs_list から削除することです。
void
ReferenceProcessor::process_phase1(DiscoveredList& refs_list,
ReferencePolicy* policy,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc) {
DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
// どのソフト リーチ可能な ref を維持するかを決定する
while (iter.has_next()) { iter.load_ptrs(DEBUG_ONLY(!discovery_is_atomic() /* allow_null_referent */)); / / 参照されたオブジェクトが生きているかどうかを判断するbool referent_is_dead = (iter.referent() != NULL) && !iter.is_referent_alive(); //参照されたオブジェクトが生きていない場合、対応する ReferencePolicy を呼び出して、オブジェクトは時々リサイクルされ ます “) ポリシーによる” , (void *)iter.obj(), iter.obj()->klass()->internal_name()); }
// リストから Reference オブジェクトを削除する
iter.remove();
// Reference オブジェクトを再びアクティブにします
iter.make_active();
//
iter.make_referent_alive();の周りの指示対象を保持します。
iter.move_to_next();
} そうでなければ { iter.next(); } } … }
refs_lists は、この GC によって発見された特定の参照タイプ (仮想参照、ソフト参照、弱参照など) を格納し、process_discovered_reflist メソッドの機能は、refs_lists から再利用する必要のないオブジェクトを削除し、最後に残った要素を削除することです。 of refs_lists リサイクルが必要なすべての要素、そして最後に最初の要素が上記の Reference.java#pending フィールドに割り当てられます。
ReferencePolicy には、NeverClearPolicy、AlwaysClearPolicy、LRUCurrentHeapPolicy、LRUMaxHeapPolicy の 4 つの実装があります。
その中で、NeverClearPolicy は常に false を返すため、SoftReference は再利用されません. JVM では、このクラスは使用されず、AlwaysClearPolicy は常に true を返します. referenceProcessor.hpp#setup メソッドでは、AlwaysClearPolicy にポリシーを設定できます. AlwaysClearPolicy がいつ使用されるかについては、興味のある人は自分で調べてください。
LRUCurrentHeapPolicy と LRUMaxHeapPolicy の should_clear_reference メソッドはまったく同じです。
bool LRUMaxHeapPolicy::should_clear_reference(oop p,
jlong timestamp_clock) { jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp§; assert(interval >= 0, “健全性チェック”);
// 前回のスカベンジ/gc 以降に参照がアクセスされた場合、間隔はゼロになります。
if (間隔 <= _max_interval) { false を返します。}
true を返します。
}
timestamp_clock は SoftReference の static フィールドのクロックであり、java_lang_ref_SoftReference::timestamp§ はフィールドのタイムスタンプに対応します。最後の GC の後に SoftReference#get が呼び出された場合、間隔の値は 0 になります。それ以外の場合は、複数の GC 間の時間差です。
_max_interval は臨界値を表し、その値は LRUCurrentHeapPolicy と LRUMaxHeapPolicy で異なります。
void LRUCurrentHeapPolicy::setup() { _max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB; assert(_max_interval >= 0,“健全性チェック”); }
void LRUMaxHeapPolicy::setup() { size_t max_heap = MaxHeapSize; max_heap -= Universe::get_heap_used_at_last_gc(); max_heap /= M;
_max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
assert(_max_interval >= 0,“健全性チェック”);
}
その中で、SoftRefLRUPolicyMSPerMB のデフォルトは 1000 です。前者の計算方法は、最後の GC 後に使用可能なヒープ サイズに関連し、後者の計算方法は (ヒープ サイズ - 最後の GC でのヒープ使用サイズ) に関連します。ここでは建築学習交流サークルを皆さんにお勧めします。コミュニケーション学習ガイド Pseudo Xin: 1253431195 (多くのインタビューの質疑応答があります)。シニア アーキテクトが記録したビデオ録画を共有します: Spring、MyBatis、Netty ソース コード分析、高同時実行性、高パフォーマンス、分散型、マイクロサービス アーキテクチャJVM パフォーマンス最適化の原則、分散アーキテクチャなどは、アーキテクトにとって必要な知識システムになっています。また、現在多くの恩恵を受けている無料の学習リソースを受け取ることもできます
これを見ると、SoftReference がいつ回収されるかがわかります。これは、使用されているポリシー (デフォルトは LRUCurrentHeapPolicy である必要があります)、使用可能なヒープ サイズ、および SoftReference が最後に get メソッドを呼び出した時刻に関連しています。
弱参照
public class WeakReference extends Reference {
public WeakReference(T referent) {
super(referent);
}
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
WeakReference は Java 層の Reference のみを変更なしで継承していることがわかります。参照対象フィールドが null に設定されるのはいつですか? これを理解するために、上記の process_discovered_reflist メソッドを見てみましょう。
size_t
ReferenceProcessor::process_discovered_reflist(
DiscoveredList refs_lists[],
ReferencePolicy* policy,
bool clear_referent,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc,
AbstractRefProcTaskExecutor* task_executor)
{ …
//フェーズ 1: 生きていないが、refs_lists からリサイクルできないすべてのソフト参照を削除します (refs_lists がソフト参照の場合のみ、ここでのポリシーは null ではありません)
if (policy != NULL) { if (mt_processing) { RefProcPhase1Task phase1 (*this, refs_lists, policy, true / marks_oops_alive /); task_executor->execute(phase1); } else { for (uint i = 0; i < _max_num_q; i++) { process_phase1(refs_lists[i], policy , is_alive, keep_alive, complete_gc); } } } else { // policy == NULL assert(refs_lists != _discoveredSoftRefs, "ソフト参照にはポリシーを指定する必要があります。"); }
// フェーズ 2:
// まだ生きているオブジェクトへのすべての参照を削除する
if (mt_processing) { RefProcPhase2Task phase2(*this, refs_lists, !discovery_is_atomic() / marks_oops_alive /); task_executor->execute(phase2); } else { for (uint i = 0; i < _max_num_q; i++) { process_phase2(refs_lists[i], is_alive, keep_alive, complete_gc); } }
// フェーズ 3: // clear_referent の
値に従ってデッド オブジェクトをリサイクルするかどうかを決定します{ for (uint i = 0; i < _max_num_q; i++) { process_phase3(refs_lists[i], clear_referent, is_alive, keep_alive, complete_gc); } }
total_list_count を返します。
}
void
ReferenceProcessor::process_phase3(DiscoveredList& refs_list,
bool clear_referent,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc) { ResourceMark rm; DiscoveredListIterator iter(refs_list, keep_alive, is_alive); while (iter.has_next()) { iter.update_discovered (); iter.load_ptrs(DEBUG_ONLY(false /* allow_null_referent */)); if (clear_referent) { // NULL out referent pointer // Reference の referent フィールドを null に設定すると、GC iterによって再利用されます。 clear_referent() ; } else { // リファレントを維持します// 参照されたオブジェクトを生きているものとしてマークし、オブジェクトはこの GC で再利用されませんiter.make_referent_alive(); } …
}
…
}
弱参照であろうと他の参照型であろうと、フィールド参照先を null に設定する操作は process_phase3 で発生し、具体的な動作は clear_referent の値によって決定されます。clear_referent の値は、参照タイプに関連しています。
ReferenceProcessorStats ReferenceProcessor::process_discovered_references(
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc,
AbstractRefProcTaskExecutor* task_executor,
GCTimer* gc_timer) { NOT_PRODUCT(verify_ok_to_handle_reflists()); … //process_discovered_reflist メソッドの 3 番目のフィールドは clear_referent です// ソフト参照size_t soft_count = 0; { GCTraceTime tt(“SoftReference”, trace_time, false, gc_timer); soft_count = process_discovered_reflist(_discoveredSoftRefs, _current_soft_ref_policy, true, is_alive, keep_alive, complete_gc, task_executor); }
update_soft_ref_master_clock();
// 弱参照
size_t weak_count = 0;
{ GCTraceTime tt(“WeakReference”, trace_time, false, gc_timer); weak_count = process_discovered_reflist(_discoveredWeakRefs, NULL, true, is_alive, keep_alive, complete_gc, task_executor); }
// 最終参照
size_t final_count = 0;
{ GCTraceTime tt(“FinalReference”, trace_time, false, gc_timer); final_count = process_discovered_reflist(_discoveredFinalRefs, NULL, false, is_alive, keep_alive, complete_gc, task_executor); }
// ファントム参照
size_t phantom_count = 0;
{ GCTraceTime tt(“PhantomReference”, trace_time, false, gc_timer); phantom_count = process_discovered_reflist(_discoveredPhantomRefs, NULL, false, is_alive, keep_alive, complete_gc, task_executor) ; } … } 、ソフト参照と弱い参照の場合、clear_referent フィールドは true で渡されます。これも期待どおりです。オブジェクトが到達不能になった後、参照フィールドは null に設定され、オブジェクトは再利用されます (ソフト参照の場合)。十分なメモリがある場合、フェーズ 1 で関連する参照が refs_list から削除され、フェーズ 3 で refs_list が空のセットになります)。
ただし、Final 参照と Phantom 参照の場合、clear_referent フィールドは false で渡されます。つまり、これら 2 つの参照タイプによって参照されるオブジェクトは、他に追加の処理がなければ、参照オブジェクトがまだ生きている限り、参照されるオブジェクトはリサイクルされません。final 参照は、オブジェクトが finalize メソッドをオーバーライドするかどうかに関連していますが、これはこの記事の分析の範囲外です. 次に Phantom 参照を見てみましょう.
PhantomReference
public class PhantomReference<T> extends Reference<T> {
public T get() {
return null;
}
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
仮想参照の get メソッドが常に null を返すことがわかります。デモを見てみましょう。
public static void demo() throws InterruptedException {
Object obj = new Object();
ReferenceQueue<Object> refQueue =new ReferenceQueue<>();
PhantomReference<Object> phanRef =new PhantomReference<>(obj, refQueue);
Object objg = phanRef.get();
//这里拿到的是null
System.out.println(objg);
//让obj变成垃圾
obj=null;
System.gc();
Thread.sleep(3000);
//gc后会将phanRef加入到refQueue中
Reference<? extends Object> phanRefP = refQueue.remove();
//这里输出true
System.out.println(phanRefP==phanRef);
}
上記のコードからわかるように、仮想参照は、指定されたオブジェクトに到達できない場合に「通知」を受け取ることができます (実際、参照を継承するすべてのクラスにはこの関数があります) 。 .referent はまだ以前に作成されたオブジェクトを指しています。これは、オブジェクト オブジェクトがリサイクルされていないことを意味します。
この現象の理由については、前のセクションの最後で説明しました。Final 参照と Phantom 参照の場合、clear_referent フィールドは false として渡されます。処理中、GC ではリサイクルされません。
仮想参照の場合、refQueue.remove(); から参照オブジェクトを取得した後、clear メソッドを呼び出して、参照とオブジェクトの間の関係を強制的に削除し、次に GC できるようになったときにオブジェクトをリサイクルできるようにします。
終わり
記事の冒頭で提起された質問に応えて、分析を読んだ後、私たちは答えを出すことができました:
1. インターネット上でソフト参照の導入をよく目にします: メモリが不足している場合にのみ再利用されます. メモリ不足はどのように定義されますか? なぜメモリ不足と呼ばれるのですか?
ソフト参照は、メモリが不足している場合に再利用されます. メモリ不足の定義は、参照オブジェクトの取得時間と現在のヒープ使用可能なメモリ サイズに関連しています. 計算式も上記に示します.
2. インターネット上での仮想参照の導入: 単なる名前であり、他の参照とは異なり、仮想参照はオブジェクトのライフ サイクルを決定しません。主に、ガベージ コレクターによって回収されるオブジェクトのアクティビティを追跡するために使用されます。本当か?
厳密に言えば、仮想参照はオブジェクトのライフサイクルに影響を与えます. 何もしなければ、仮想参照がリサイクルされない限り、それらが参照するオブジェクトは決してリサイクルされません. したがって、一般に、ReferenceQueue から PhantomReference オブジェクトを取得した後、PhantomReference オブジェクトが再利用されない場合 (たとえば、GC ROOT によって到達可能な他のオブジェクトによって参照されている場合)、clear メソッドを呼び出して、オブジェクト間の参照関係を解放する必要があります。 PhantomReference とその参照オブジェクト。
3. Jdk で仮想参照が使用されるのはどのシナリオですか?
DirectByteBuffer では、仮想参照 Cleaner.java のサブクラスを使用して、オフヒープ メモリの回復を実現していますが、オフヒープ メモリの内部と外部について説明する記事を書きます。