mmtk / mmtk-core

Memory Management ToolKit
https://www.mmtk.io
Other
379 stars 69 forks source link

ReferenceProcessor does not trace transitively discovered SoftReference instances #1125

Closed wks closed 7 months ago

wks commented 7 months ago

After retaining and scanning SoftReference, it generates ProcessEdgesWork work packets to trace the children of retained SoftReference instances. During this time, more SoftReference instances will be discovered, but they will not be traced. That will cause those transitively discovered SoftReference to contain un-updated reference to the children in the from-space, or crash MarkCompact due to those object not having forwarding pointers.

This bug was originally discovered when running CI tests for the PR removing ObjectReference::NULL when running fop in DaCapo 2006 using MarkCompact. See: https://github.com/mmtk/mmtk-openjdk/actions/runs/8750247972/job/24016142656?pr=265

The program

import java.lang.ref.*;

class StrongNode {
    public WeakNode next;
    public StrongNode(WeakNode next) {
        this.next = next;
    }
}

class WeakNode extends SoftReference<StrongNode> {
    public WeakNode(StrongNode next) {
        super(next);
    }
}

public class TwoLevelDeadWeak {
    public static WeakNode mkWeak(Object object) {
        StrongNode strong1 = new StrongNode(null);
        WeakNode weak1 = new WeakNode(strong1);
        StrongNode strong2 = new StrongNode(weak1);
        WeakNode weak2 = new WeakNode(strong2);
        return weak2;
    }

    public static void main(String[] args) {
        Object obj = new Object();

        System.out.println("Making objects");
        WeakNode rooted = mkWeak(obj);
        System.out.println("Triggering GC...");
        System.gc();

        System.out.format("Rooted: %s%n", rooted);
        StrongNode myStrong = rooted.get().next.get();
        System.out.format("myStrong: %s%n", myStrong);

        System.out.println("Done.");
    }
}

How to crash SemiSpace?

Run it with MMTK_NO_REFERENCE_TYPE=true, preferrably with stack trace and logs enabled.

export RUST_LOG=info,mmtk::util::reference_processor=trace
export RUST_BACKTRACE=1
MMTK_THREADS=1 MMTK_NO_REFERENCE_TYPES=false MMTK_PLAN=SemiSpace /path/to/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin/java -XX:+UseThirdPartyHeap -cp /path/to/the/program TwoLevelDeadWeak

It will print:

...
[2024-04-19T12:08:53Z TRACE mmtk::util::reference_processor] Process reference: 0x400aef68
[2024-04-19T12:08:53Z TRACE mmtk::util::reference_processor]  ~> 0x400e0410
[2024-04-19T12:08:53Z TRACE mmtk::util::reference_processor]  => 0x40473250
[2024-04-19T12:08:53Z TRACE mmtk::util::reference_processor]  ~> 0x404853d0
[2024-04-19T12:08:53Z DEBUG mmtk::util::reference_processor] SOFT reference table from 151 to 100 (0 enqueued)
[2024-04-19T12:08:53Z DEBUG mmtk::util::reference_processor] Ending ReferenceProcessor.scan(SOFT)
[2024-04-19T12:08:53Z TRACE mmtk::util::reference_processor] Add soft candidate: 0x404e8480
[2024-04-19T12:08:53Z DEBUG mmtk::util::reference_processor] Starting ReferenceProcessor.scan(WEAK)
[2024-04-19T12:08:53Z TRACE mmtk::util::reference_processor] WEAK Reference table is {0x40477d30, 0x400b14f8, ...
...
[2024-04-19T12:08:53Z TRACE mmtk::util::reference_processor] Process reference: 0x4046f870
[2024-04-19T12:08:53Z TRACE mmtk::util::reference_processor]  ~> 0x0
[2024-04-19T12:08:53Z TRACE mmtk::util::reference_processor]  (cleared referent) 
[2024-04-19T12:08:53Z DEBUG mmtk::util::reference_processor] PHANTOM reference table from 42 to 3 (10 enqueued)
[2024-04-19T12:08:53Z DEBUG mmtk::util::reference_processor] Ending ReferenceProcessor.scan(PHANTOM)
thread '<unnamed>' panicked at /home/wks/projects/mmtk-github/mmtk-core/src/util/reference_processor.rs:252:21:
Referent 0x401ebf18 (of reference 0x404e8480) is not in any space
stack backtrace:
   0: rust_begin_unwind
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/std/src/panicking.rs:647:5
   1: core::panicking::panic_fmt
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/core/src/panicking.rs:72:14
   2: mmtk::util::reference_processor::ReferenceProcessor::enqueue::{{closure}}
             at /home/wks/projects/mmtk-github/mmtk-core/src/util/reference_processor.rs:252:21
   3: core::iter::traits::iterator::Iterator::for_each::call::{{closure}}
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/core/src/iter/traits/iterator.rs:855:29
   4: <hashbrown::map::Keys<K,V> as core::iter::traits::iterator::Iterator>::fold::{{closure}}
             at /rust/deps/hashbrown-0.14.3/src/map.rs:4865:45
   5: <hashbrown::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::fold::{{closure}}
             at /rust/deps/hashbrown-0.14.3/src/map.rs:4749:13
   6: hashbrown::raw::RawIterRange<T>::fold_impl
             at /rust/deps/hashbrown-0.14.3/src/raw/mod.rs:3885:23
   7: <hashbrown::raw::RawIter<T> as core::iter::traits::iterator::Iterator>::fold
             at /rust/deps/hashbrown-0.14.3/src/raw/mod.rs:4156:18
   8: <hashbrown::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::fold
             at /rust/deps/hashbrown-0.14.3/src/map.rs:4747:20
   9: <hashbrown::map::Keys<K,V> as core::iter::traits::iterator::Iterator>::fold
             at /rust/deps/hashbrown-0.14.3/src/map.rs:4865:9
  10: <hashbrown::set::Iter<K> as core::iter::traits::iterator::Iterator>::fold
             at /rust/deps/hashbrown-0.14.3/src/set.rs:1705:9
  11: <std::collections::hash::set::Iter<K> as core::iter::traits::iterator::Iterator>::fold
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/std/src/collections/hash/set.rs:1513:19
  12: core::iter::traits::iterator::Iterator::for_each
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/core/src/iter/traits/iterator.rs:858:9
  13: mmtk::util::reference_processor::ReferenceProcessor::enqueue
             at /home/wks/projects/mmtk-github/mmtk-core/src/util/reference_processor.rs:247:13
  14: mmtk::util::reference_processor::ReferenceProcessors::enqueue_refs
             at /home/wks/projects/mmtk-github/mmtk-core/src/util/reference_processor.rs:60:9
  15: <mmtk::util::reference_processor::RefEnqueue<VM> as mmtk::scheduler::work::GCWork<VM>>::do_work
             at /home/wks/projects/mmtk-github/mmtk-core/src/util/reference_processor.rs:559:9
  16: mmtk::scheduler::work::GCWork::do_work_with_stat
             at /home/wks/projects/mmtk-github/mmtk-core/src/scheduler/work.rs:45:9
  17: mmtk::scheduler::worker::GCWorker<VM>::run
             at /home/wks/projects/mmtk-github/mmtk-core/src/scheduler/worker.rs:244:13
  18: mmtk::memory_manager::start_worker
             at /home/wks/projects/mmtk-github/mmtk-core/src/memory_manager.rs:470:5
  19: start_worker
             at /home/wks/projects/mmtk-github/mmtk-openjdk/mmtk/src/api.rs:214:9
  20: _ZN19MMTkCollectorThread3runEv
             at /home/wks/projects/mmtk-github/openjdk/../mmtk-openjdk/openjdk/mmtkCollectorThread.cpp:36:15
  21: _ZN6Thread8call_runEv
             at /home/wks/projects/mmtk-github/openjdk/src/hotspot/share/runtime/thread.cpp:402:12
  22: thread_native_entry
             at /home/wks/projects/mmtk-github/openjdk/src/hotspot/os/linux/os_linux.cpp:826:19
  23: <unknown>
  24: <unknown>
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
fatal runtime error: failed to initiate panic, error 5
fish: Job 1, 'MMTK_THREADS=1 MMTK_NO_REFERENC…' terminated by signal SIGABRT (Abort)

How to crash MarkCompact

We first patch mmtk-core so that MarkCompactSpace::trace_forward_object asserts the object has forwarding pointer. If an object is reachable during the SecondRoot stage or the RefForwarding stage, it must be live, and its forwarding pointer must have been computed during the CalculateForwarding stage.

diff --git a/src/policy/markcompactspace.rs b/src/policy/markcompactspace.rs
index e6d332919..533d7fa8c 100644
--- a/src/policy/markcompactspace.rs
+++ b/src/policy/markcompactspace.rs
@@ -251,7 +251,9 @@ impl<VM: VMBinding> MarkCompactSpace<VM> {
             queue.enqueue(object);
         }

-        Self::get_header_forwarding_pointer(object)
+        let result = Self::get_header_forwarding_pointer(object);
+        assert!(!result.is_null(), "Object {object} does not have a forwarding pointer");
+        result
     }

     pub fn test_and_mark(object: ObjectReference) -> bool {

Then run the same program with the same env vars except using MarkCompact. It will crash with the following log:

...
[2024-04-19T12:15:46Z DEBUG mmtk::util::reference_processor] SOFT reference table from 101 to 100 (0 enqueued)
[2024-04-19T12:15:46Z DEBUG mmtk::util::reference_processor] Ending ReferenceProcessor.scan(SOFT)
[2024-04-19T12:15:46Z TRACE mmtk::util::reference_processor] Add soft candidate: 0x4066bf28
[2024-04-19T12:15:46Z DEBUG mmtk::util::reference_processor] Starting ReferenceProcessor.scan(WEAK)
...
[2024-04-19T12:15:46Z TRACE mmtk::util::reference_processor] Forwarding reference: 0x4066bf28 (size: 40)
thread '<unnamed>' panicked at /home/wks/projects/mmtk-github/mmtk-core/src/policy/markcompactspace.rs:255:9:
Object 0x4066bf10 does not have a forwarding pointer
stack backtrace:
   0: rust_begin_unwind
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/std/src/panicking.rs:647:5
   1: core::panicking::panic_fmt
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/core/src/panicking.rs:72:14
   2: mmtk::policy::markcompactspace::MarkCompactSpace<VM>::trace_forward_object
             at /home/wks/projects/mmtk-github/mmtk-core/src/policy/markcompactspace.rs:255:9
   3: <mmtk::policy::markcompactspace::MarkCompactSpace<VM> as mmtk::policy::gc_work::PolicyTraceObject<VM>>::trace_object
             at /home/wks/projects/mmtk-github/mmtk-core/src/policy/markcompactspace.rs:141:13
   4: <mmtk::plan::markcompact::global::MarkCompact<VM> as mmtk::plan::global::PlanTraceObject<VM>>::trace_object
             at /home/wks/projects/mmtk-github/mmtk-core/src/plan/markcompact/global.rs:30:21
   5: <mmtk::scheduler::gc_work::PlanProcessEdges<VM,P,_> as mmtk::scheduler::gc_work::ProcessEdgesWork>::trace_object
             at /home/wks/projects/mmtk-github/mmtk-core/src/scheduler/gc_work.rs:931:9
   6: mmtk::util::reference_processor::ReferenceProcessor::get_forwarded_referent
             at /home/wks/projects/mmtk-github/mmtk-core/src/util/reference_processor.rs:220:9
   7: mmtk::util::reference_processor::ReferenceProcessor::forward::forward_reference
             at /home/wks/projects/mmtk-github/mmtk-core/src/util/reference_processor.rs:301:36
   8: mmtk::util::reference_processor::ReferenceProcessor::forward::{{closure}}
             at /home/wks/projects/mmtk-github/mmtk-core/src/util/reference_processor.rs:325:25
   9: core::iter::adapters::map::map_fold::{{closure}}
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/core/src/iter/adapters/map.rs:89:28
  10: <hashbrown::map::Keys<K,V> as core::iter::traits::iterator::Iterator>::fold::{{closure}}
             at /rust/deps/hashbrown-0.14.3/src/map.rs:4865:45
  11: <hashbrown::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::fold::{{closure}}
             at /rust/deps/hashbrown-0.14.3/src/map.rs:4749:13
  12: hashbrown::raw::RawIterRange<T>::fold_impl
             at /rust/deps/hashbrown-0.14.3/src/raw/mod.rs:3885:23
  13: <hashbrown::raw::RawIter<T> as core::iter::traits::iterator::Iterator>::fold
             at /rust/deps/hashbrown-0.14.3/src/raw/mod.rs:4156:18
  14: <hashbrown::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::fold
             at /rust/deps/hashbrown-0.14.3/src/map.rs:4747:20
  15: <hashbrown::map::Keys<K,V> as core::iter::traits::iterator::Iterator>::fold
             at /rust/deps/hashbrown-0.14.3/src/map.rs:4865:9
  16: <hashbrown::set::Iter<K> as core::iter::traits::iterator::Iterator>::fold
             at /rust/deps/hashbrown-0.14.3/src/set.rs:1705:9
  17: <std::collections::hash::set::Iter<K> as core::iter::traits::iterator::Iterator>::fold
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/std/src/collections/hash/set.rs:1513:19
  18: <core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::fold
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/core/src/iter/adapters/map.rs:129:9
  19: <core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::fold
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/core/src/iter/adapters/map.rs:129:9
  20: core::iter::traits::iterator::Iterator::for_each
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/core/src/iter/traits/iterator.rs:858:9
  21: <hashbrown::map::HashMap<K,V,S,A> as core::iter::traits::collect::Extend<(K,V)>>::extend
             at /rust/deps/hashbrown-0.14.3/src/map.rs:6511:9
  22: <hashbrown::set::HashSet<T,S,A> as core::iter::traits::collect::Extend<T>>::extend
             at /rust/deps/hashbrown-0.14.3/src/set.rs:1355:9
  23: <std::collections::hash::set::HashSet<T,S> as core::iter::traits::collect::Extend<T>>::extend
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/std/src/collections/hash/set.rs:1070:9
  24: <std::collections::hash::set::HashSet<T,S> as core::iter::traits::collect::FromIterator<T>>::from_iter
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/std/src/collections/hash/set.rs:1026:13
  25: core::iter::traits::iterator::Iterator::collect
             at /rustc/aedd173a2c086e558c2b66d3743b344f977621a7/library/core/src/iter/traits/iterator.rs:2054:9
  26: mmtk::util::reference_processor::ReferenceProcessor::forward
             at /home/wks/projects/mmtk-github/mmtk-core/src/util/reference_processor.rs:322:27
  27: mmtk::util::reference_processor::ReferenceProcessors::forward_refs
             at /home/wks/projects/mmtk-github/mmtk-core/src/util/reference_processor.rs:74:9
  28: <mmtk::util::reference_processor::RefForwarding<E> as mmtk::scheduler::work::GCWork<<E as mmtk::scheduler::gc_work::ProcessEdgesWork>::VM>>::do_work
             at /home/wks/projects/mmtk-github/mmtk-core/src/util/reference_processor.rs:545:9
  29: mmtk::scheduler::work::GCWork::do_work_with_stat
             at /home/wks/projects/mmtk-github/mmtk-core/src/scheduler/work.rs:45:9
  30: mmtk::scheduler::worker::GCWorker<VM>::run
             at /home/wks/projects/mmtk-github/mmtk-core/src/scheduler/worker.rs:244:13
  31: mmtk::memory_manager::start_worker
             at /home/wks/projects/mmtk-github/mmtk-core/src/memory_manager.rs:470:5
  32: start_worker
             at /home/wks/projects/mmtk-github/mmtk-openjdk/mmtk/src/api.rs:214:9
  33: _ZN19MMTkCollectorThread3runEv
             at /home/wks/projects/mmtk-github/openjdk/../mmtk-openjdk/openjdk/mmtkCollectorThread.cpp:36:15
  34: _ZN6Thread8call_runEv
             at /home/wks/projects/mmtk-github/openjdk/src/hotspot/share/runtime/thread.cpp:402:12
  35: thread_native_entry
             at /home/wks/projects/mmtk-github/openjdk/src/hotspot/os/linux/os_linux.cpp:826:19
  36: <unknown>
  37: <unknown>
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
fatal runtime error: failed to initiate panic, error 5
fish: Job 1, 'MMTK_THREADS=1 MMTK_NO_REFERENC…' terminated by signal SIGABRT (Abort)

What went wrong?

This portion of the log reveals the cause:

[2024-04-19T12:15:46Z DEBUG mmtk::util::reference_processor] Ending ReferenceProcessor.scan(SOFT)
[2024-04-19T12:15:46Z TRACE mmtk::util::reference_processor] Add soft candidate: 0x4066bf28
[2024-04-19T12:15:46Z DEBUG mmtk::util::reference_processor] Starting ReferenceProcessor.scan(WEAK)

A new soft candidate is discovered after processing all soft references. The OpenJDK binding discovers soft/weak/phantom references gradually during tracing. The new soft candidate 0x4066bf28 was added. Itself was traced. But when it was scanned, it added itself to the reference processor using add_soft_candidate. However, it never gets processed, and its referent is not traced, either.

In SemiSpace, the referent pointer still pointed to the from-space. During the Release stage, it tried to find references to enqueue, and the debug assertion discovered that some SoftReference still pointed to the from space.

In MarkCompact, things were a bit complicated. During CalculateForwarding, the GC computeed the forwarded addresses of all live objects. However, since the referent of the newly discovered SoftReference was not marked, it was considered dead, and was not assigned a forwarding pointer. However, during SecondRoot, the GC computed another transitive closure and found the SoftReference to be live. It then attempted to forward the referent (which is dead), and found it did not have a forwarding pointer. The error would have silently slipped away without checking, but after I removed ObjectReference::NULL, it started to require an explicit NULL check against the forwarding pointer, and caught the bug.

qinsoon commented 7 months ago

I think we had some discussion about soft reference in https://github.com/mmtk/mmtk-core/pull/564#issuecomment-1097453686 and https://github.com/mmtk/mmtk-core/pull/564#pullrequestreview-940369329. At that time, we concluded it was fine to not transitively trace soft references.

So the actual issue is that we add more soft references to the soft reference list. They are not scanned, and not retained, but we attempt to forward those references. Is this understanding correct?

wks commented 7 months ago

I think we had some discussion about soft reference in #564 (comment) and #564 (review). At that time, we concluded it was fine to not transitively trace soft references.

Yes. The Java API allows different policies for retaining SoftReference to be implemented. It's OK not to retain transitively reachable SoftReference.

So the actual issue is that we add more soft references to the soft reference list. They are not scanned, and not retained, but we attempt to forward those references. Is this understanding correct?

Exactly. They should either be retained or cleared, but it's currently neither. And we attempted to forward the referent (The SoftReference itself is safe to forward because it is marked as an ordinary object, but not scanned, yet.) which is not marked.

The easiest fix should be clearing the newly added SoftReference instances. There is a comment in the reference processor:

    fn retain<E: ProcessEdgesWork>(&self, trace: &mut E, _nursery: bool) {
        // ...
        for reference in sync.references.iter() {
            // ...
            if !reference.is_live::<E::VM>() {
                // Reference is currently unreachable but may get reachable by the
                // following trace. We postpone the decision.
                continue;
            }
            // ...
        }
        // ...
    }

We postponed the decision, but we didn't make decision at the right time. See:

    pub fn scan_soft_refs<E: ProcessEdgesWork>(&self, trace: &mut E, mmtk: &'static MMTK<E::VM>) {
        if !mmtk.state.is_emergency_collection() {
            self.soft.retain::<E>(trace, is_nursery_gc(mmtk.get_plan()));
        }

        self.soft.scan::<E>(trace, is_nursery_gc(mmtk.get_plan())); // ERROR: It's too early!!!!!!!!!!!
    }

The call to self.soft.scan happens too early. By retaining soft references, we extended the transitive closure of live objects. This means two things: (1) More live objects will be discovered, and more importantly, (2) More live SoftReference (and other forms of references) will be discovered. We should finish the transitive closure before calling self.soft.scan.

One way to implement it is making use of the "bucket sentinel" feature I introduced for implementing VM-side reference processing. If we choose to retain soft references, we should postpone self.soft.scan to a "sentinel packet" at the end of the SoftRefClosure bucket.

BTW, I think the bucket names WeakRefClosure and PhantomRefClosure are misnomers. Weak references and phantom references never expand the transitive closure. (On the contrary, soft references and finalizers do.) Weak and phantom references should be either forwarded or cleared, but never enqueue any objects.

wks commented 7 months ago

A related bug is that finalizable objects can also expand the transitive closure when resurrected. If a finalizable object points to a WeakReference (or other kinds of references), the reference will not be forwarded or cleared, either. The following program is guaranteed to crash MarkCompact and SemiSpace, too:

import java.lang.ref.*;

class Bar {
}

class Foo {
    public static Foo resurrectedFoo = null;

    public WeakReference<Bar> weak;

    public Foo() {
        this.weak = new WeakReference<>(new Bar());
    }

    @Override
    public void finalize() {
        System.out.println("I am live again!");
        resurrectedFoo = this;
    }
}

public class FinalAndWeak {
    public static void mkObject() {
        Foo foo = new Foo();
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("Making object...");
        mkObject();

        System.out.println("Triggering GC...");
        System.gc();

        while (Foo.resurrectedFoo == null) {
            System.out.println("Waiting for Foo.finalize()...");
            Thread.sleep(100);
        }

        Foo theFoo = Foo.resurrectedFoo;
        System.out.println(theFoo.weak.get());
    }
}

To fix this problem, we need to re-scan soft and weak references after resurrecting finalizable objects.

wks commented 7 months ago

By re-scanning soft and weak references after resurrecting finalizable objects, the crash disappeared.

However, something is still inconsistent. For example:

import java.lang.ref.*;

class Bar {
    int num;

    public Bar(int num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return String.format("Bar(%d)", this.num);
    }
}

class Foo {
    public static Foo resurrectedFoo = null;

    public WeakReference<Bar> weak1;
    public WeakReference<Bar> weak2;
    public Bar bar2;

    public Foo(Bar bar1, Bar bar2) {
        this.weak1 = new WeakReference<>(bar1);
        this.weak2 = new WeakReference<>(bar2);
        this.bar2 = bar2;
    }

    @Override
    public void finalize() {
        System.out.println("I am live again!");
        resurrectedFoo = this;
    }
}

public class FinalAndWeak {
    public static WeakReference<Bar> mkObject() {
        Bar bar1 = new Bar(1);
        Bar bar2 = new Bar(2);
        Foo foo = new Foo(bar1, bar2);
        WeakReference<Bar> ref2 = new WeakReference<>(bar2);
        return ref2;
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("Making object...");
        WeakReference<Bar> ref2 = mkObject();

        System.out.println("Triggering GC...");
        System.gc();

        while (Foo.resurrectedFoo == null) {
            System.out.println("Waiting for Foo.finalize()...");
            Thread.sleep(100);
        }

        Foo theFoo = Foo.resurrectedFoo;
        System.out.format("theFoo.weak1.get() == %s%n", theFoo.weak1.get());
        System.out.format("theFoo.weak2.get() == %s%n", theFoo.weak2.get());
        System.out.format("theFoo.bar2 == %s%n", theFoo.bar2);
        System.out.format("ref2.get() == %s%n", ref2.get());
    }
}

There is a weak reference from the root to Bar(2), and there is also a strong and a weak reference from Foo to Bar(2). However, after the GC, the WeakReference<Bar> ref2 in main is cleared, but the WeakReference<Bar> weak2 in Foo is not. Here is the result when running mmtk-openjdk after my fix:

[2024-04-20T12:18:10Z INFO  mmtk::scheduler::scheduler] End of GC (672/51200 pages, took 14 ms)
I am live again!
Waiting for Foo.finalize()...
theFoo.weak1.get() == null
theFoo.weak2.get() == Bar(2)
theFoo.bar2 == Bar(2)
ref2.get() == null

Strictly speaking, this is not compliant with the Java API. The Java API demands that (link)

Suppose that the garbage collector determines at a certain point in time that an object is weakly reachable. At that time it will atomically clear all weak references to that object and all weak references to any other weakly-reachable objects from which that object is reachable through a chain of strong and soft references. ...

The result clearly shows that it didn't clear all weak references.

However, the official OpenJDK behaves this way, too. Here is the result from vanilla OpenJDK 22:

Triggering GC...
Waiting for Foo.finalize()...
I am live again!
theFoo.weak1.get() == Bar(1)
theFoo.weak2.get() == Bar(2)
theFoo.bar2 == Bar(2)
ref2.get() == null

I won't worry too much about this detail because the vanilla OpenJDK is not compliant, either.