Javaには、強参照、ソフト参照、弱参照、仮想参照の4つのコア参照タイプがあります。一般的に、より強い参照を使用する傾向があり、めったに他の3つの参照を使用してシーンに遭遇しないので、その把握の原則は、白紙の多くです。今回は、この4つの参考文献の原理を勉強して、自分の混乱を解決する機会です。
強力な参照については、日常的に使用するため、我々は基本的に、より明確であるため、この記事では、この作品は強力な参照を探索しません。上記の4つの参照に加えて、FinalReferenceと呼ばれる参照タイプがありますが、この記事も探求しません。本稿では、主にソフト参照、弱い参照と仮想参照の原理と違いを探ります。
ソースコード解析
JVMレイヤーとJavaレイヤーの両方が、リファレンス・リサイクル・プロセスを通じてクリーンアップに関与します。
Java
最終的なクリーンアップはJavaレイヤーが行うので、まずJavaレイヤーがエントリーポイントとして使われます。
Referenceデータ構造
リファレンスのデータ構造を見てみると便利かもしれません。
public abstract class Reference<T> {
private T referent;
volatile ReferenceQueue<? super T> queue;
Reference next;
transient private Reference<T> discovered;
private static Reference<Object> pending = null;
}
これはReferenceのデータ構造です。
- referenceは参照されるオブジェクト
- キューはクリーニングされた参照を格納するために使用され、ここではキューはチェーンに格納され、nextはチェーンの次のノードを示します。
- DiscoveredとPendingは文脈によって意味が異なるという点で興味深いです。
- 通常、discoveredはDiscoveredListを意味します。
Javaレイヤーのリサイクルコード
次に、Javaレイヤーのリサイクル・コードを見てください。
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
c = r instanceof Cleaner ? (Cleaner) r : null;
pending = r.discovered;
r.discovered = null;
} else {
if (waitForNotify) {
lock.wait();
}
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
Thread.yield();
return true;
} catch (InterruptedException x) {
return true;
}
if (c != null) {
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
private static class ReferenceHandler extends Thread {
public void run() {
while (true) {
tryHandlePending(true);
}
}
}
まず tryHandlePending メソッドを見ると、全体のロジックはまだ比較的単純であることがわかります。外側の while(true)と組み合わせることで、PendingList 全体をクリーンアップする機能が実現されます。
JVM
上記から、参照オブジェクトがPendingListに追加されるとすぐにクリーンアップされることはすでに知られています。では、これらの参照オブジェクトはいつ、どのような状況でPendingListに追加されるのでしょうか?繰り返しますが、これがソフト参照、弱い参照、ダミー参照の違いの核心です。
JVMレイヤーのコア処理コードはreferenceProcessor.cppにあり、コアメソッドはprocess_discovered_references()で、CMS GCを例にとると、このメソッドはFinalMarking(再マーキング)フェーズで呼び出され、このコードのコアロジックは次のとおりです。
ReferenceProcessorStats ReferenceProcessor::process_discovered_references(BoolObjectClosure* is_alive, OopClosure* keep_alive, VoidClosure* complete_gc, AbstractRefProcTaskExecutor* task_executor, ReferenceProcessorPhaseTimes* phase_times) {
double start_time = os::elapsedTime();
disable_discovery();
_soft_ref_timestamp_clock = java_lang_ref_SoftReference::clock();
ReferenceProcessorStats stats(total_count(_discoveredSoftRefs),
total_count(_discoveredWeakRefs),
total_count(_discoveredFinalRefs),
total_count(_discoveredPhantomRefs));
// 1. ソフト参照の初期処理
{
RefProcTotalPhaseTimesTracker tt(RefPhase1, phase_times, this);
process_soft_ref_reconsider(is_alive, keep_alive, complete_gc,
task_executor, phase_times);
}
update_soft_ref_master_clock();
// 2. ソフト参照、弱い参照、FinalReferenceを扱う
{
RefProcTotalPhaseTimesTracker tt(RefPhase2, phase_times, this);
process_soft_weak_final_refs(is_alive, keep_alive, complete_gc, task_executor, phase_times);
}
// 3. FinalReference処理ロジックのもう一方の端
{
RefProcTotalPhaseTimesTracker tt(RefPhase3, phase_times, this);
process_final_keep_alive(keep_alive, complete_gc, task_executor, phase_times);
}
// 4. ダミー参照を扱う
{
RefProcTotalPhaseTimesTracker tt(RefPhase4, phase_times, this);
process_phantom_refs(is_alive, keep_alive, complete_gc, task_executor, phase_times);
}
if (task_executor != NULL) {
task_executor->set_single_threaded_mode();
}
phase_times->set_total_time_ms((os::elapsedTime() - start_time) * 1000);
return stats;
}
今回は興味のないFinalReferenceを除くと、全体的な処理はこのようになることがお分かりいただけると思います。
- ソフトリファレンスの初期処理
- ソフトリファレンスと弱いリファレンスの扱い
- 仮想参照の処理
process_soft_ref_reconsider
このメソッドの中で、コアは以下のロジックを呼び出します。
size_t ReferenceProcessor::process_soft_ref_reconsider_work(DiscoveredList& refs_list,
ReferencePolicy* policy,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc) {
DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
while (iter.has_next()) {
bool referent_is_dead = (iter.referent() != NULL) && !iter.is_referent_alive();
if (referent_is_dead &&
!policy->should_clear_reference(iter.obj(), _soft_ref_timestamp_clock)) {
iter.remove();
iter.make_referent_alive();
iter.move_to_next();
} else {
iter.next();
}
}
complete_gc->do_void();
return iter.removed();
}
このコードでは、直訳すると、「 ソフト参照リスト内の、デッド状態にあるがクリーンアップする必要のない、つまりクリーンアップに参加しないオブジェクトをすべてキューから削除する」となります。
ここで興味深いのが、クリーンアップが必要かどうかのロジックです。過去に、ソフト参照はメモリがいっぱいになったときだけクリーンアップされると聞いたことがありますが、そうなのでしょうか、そうでないのでしょうか?
ここで、should_clear_referenceは実際には戦略パターンを使っています。つまり、この方法は状況によって異なるということで、当面は以下のようにいくつかの戦略があります。
// AlwaysClearPolicy
class AlwaysClearPolicy : public ReferencePolicy {
public:
virtual bool should_clear_reference(oop p, jlong timestamp_clock) {
return true;
}
};
// LRUCurrentHeapPolicy
bool LRUCurrentHeapPolicy::should_clear_reference(oop p,
jlong timestamp_clock) {
jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
if(interval <= _max_interval) {
return false;
}
return true;
}
// LRUMaxHeapPolicy
bool LRUMaxHeapPolicy::should_clear_reference(oop p,
jlong timestamp_clock) {
jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
if(interval <= _max_interval) {
return false;
}
return true;
}
// NeverClearPolicy
class NeverClearPolicy : public ReferencePolicy {
public:
virtual bool should_clear_reference(oop p, jlong timestamp_clock) {
return false;
}
};
まず、NeverClearPolicyは実際にはJVMによって使用されないので、ここでは無視します。AlwaysClearPolicyはGC中に使用されないので、ここでは議論しません。次にLRUCurrentHeapPolicyとLRUMaxHeapPolicyですが、この2つのポリシーはどのような状況で使用されるのでしょうか?
コードをここに載せるのはやめて、答えにたどり着きましょう。
しかし、よく見てみると、この2つのストラテジーのコードは同じように見えます。では、何が違うのでしょうか?
実際、_max_intervalの値はこの2つの戦略で以下のように異なります。
void LRUCurrentHeapPolicy::setup() {
_max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB;
}
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;
}
この2つのアプローチの長所と短所を探るつもりはありません。上記のコードから以下の結論が得られます。
ソフトリファレンスのリカバリーメカニズムは状況によって異なります。
ソフトリファレンスは、おそらくメモリ不足になったときのみ回収されます。
ソフトリファレンスの再生成のタイミングは、過去のGCデータに基づいて推定された時点であり、実際にはメモリ容量とは関係ありません。
process_soft_weak_final_refs
この方法の核となるロジックは以下の通り。
process_soft_weak_final_refs_work(_discoveredSoftRefs[i], is_alive, keep_alive, true);
process_soft_weak_final_refs_work(_discoveredWeakRefs[i], is_alive, keep_alive, true);
process_soft_weak_final_refs_work(_discoveredFinalRefs[i], is_alive, keep_alive, false);
process_soft_weak_final_refs_work()
つまり、このメソッドはソフト参照、弱参照、FinalReferencesに対してそれぞれ呼び出されます。
size_t ReferenceProcessor::process_soft_weak_final_refs_work(DiscoveredList& refs_list,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
bool do_enqueue_and_clear) {
DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
while (iter.has_next()) {
if (iter.referent() == NULL) {
iter.remove();
iter.move_to_next();
} else if (iter.is_referent_alive()) {
iter.remove();
iter.make_referent_alive();
iter.move_to_next();
} else {
if (do_enqueue_and_clear) { // ソフト参照と弱い参照が真である。
iter.clear_referent();
iter.enqueue();
}
iter.next();
}
}
if (do_enqueue_and_clear) {
iter.complete_enqueue();
refs_list.clear();
}
return iter.removed();
}
繰り返しますが、このロジックは比較的単純で、単純にこう述べています。
- 参照されたオブジェクトが空の場合、または参照されたオブジェクトがまだアクティブな場合は、キューから移動されます。
- 参照されたオブジェクトがアクティブなオブジェクトでない場合、PendingListに追加されます。
つまり、弱参照はGCまでにすべての非アクティブオブジェクトをクリーンアップしますが、ソフト参照は利用可能なポリシーのプライミングにより、必ずしもクリーンアップされません。
process_phantom_refs
このメソッドのコアの呼び出しロジックは以下の通りです。
size_t ReferenceProcessor::process_phantom_refs_work(DiscoveredList& refs_list,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc) {
DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
while (iter.has_next()) {
oop const referent = iter.referent();
if (referent == NULL || iter.is_referent_alive()) {
iter.make_referent_alive();
iter.remove();
iter.move_to_next();
} else {
iter.clear_referent();
iter.enqueue();
iter.next();
}
}
iter.complete_enqueue();
complete_gc->do_void();
refs_list.clear();
return iter.removed();
}
弱参照と弱参照の唯一の違いは、ダミー参照参照 == NULL のときに make_referent_alive 操作が実行されるかどうかということのようですが、それほど大きな違いはないようです。弱参照とダミー参照の本当の違いは、Javaレベルでは、ダミー参照のgetメソッドは常にNULLを返すということです。
概要
以上の分析から、ソフト・リファレンス、ウィーク・リファレンス、ダミー・リファレンスの違いは次のようになります。
- ソフト参照はおそらくGC時にクリーンアップされますが、頻度は低くなります。
- 弱い参照はGC時にクリーンアップされます。
- 仮想参照参照オブジェクトを直接使用することはできませんが、主なアプリケーションのシナリオは、GCの時間でも必然的にクリーンアップされますゴミの収集を追跡ReferenceQueueと協力することです。
[1]