Skip to content

8387790: Shenandoah: Remove degenerated cycles - #31797

Open
earthling-amzn wants to merge 143 commits into
openjdk:masterfrom
earthling-amzn:remove-degenerated-cycles
Open

8387790: Shenandoah: Remove degenerated cycles#31797
earthling-amzn wants to merge 143 commits into
openjdk:masterfrom
earthling-amzn:remove-degenerated-cycles

Conversation

@earthling-amzn

@earthling-amzn earthling-amzn commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

When Shenandoah experiences a concurrent mode failure, it initiates a safepoint and attempts to finish the collection cycle there. This handoff between concurrent and degenerated cycles is error prone and has been the source of many, many defects. Removing degenerated cycles will simplify the code base, lower maintenance costs and allow the team to focus on more meaningful changes.

Threads which cannot allocate will stall. Threads which cannot evacuate will self-forward the object (this is not new behavior). Memory from regions with self-forwarded objects will be reclaimed simply by moving the region's top to the highest self-forwarded object in the region. More sophisticated reclamation from such regions will follow.

Additional behavior changes:

  • An allocation failure during old marking will run a global collection, rather than run young and risk starving old.
  • Allocation stalls are reported at the end of the process, reporting them for each cycle is deferred to another PR.
  • Allocation stalls are also used as a signal to increase the concurrent worker thread count (up to parallel thread count).
  • A Full GC will run after a thread cannot allocate after ShenandoahFullGCThreshold consecutive concurrent GCs.
  • The ShenandoahRegionCounter (used exclusively for the visualizer) reports cycles with evacuation failures instead of degenerated cycles.


Progress

  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change must be properly reviewed (2 reviews required, with at least 1 Reviewer, 1 Author)

Issue

  • JDK-8387790: Shenandoah: Remove degenerated cycles (Enhancement - P4)

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/31797/head:pull/31797
$ git checkout pull/31797

Update a local copy of the PR:
$ git checkout pull/31797
$ git pull https://git.openjdk.org/jdk.git pull/31797/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 31797

View PR using the GUI difftool:
$ git pr show -t 31797

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/31797.diff

Using Webrev

Link to Webrev Comment

… from updating references

Otherwise, assertions about objects being forwarded (even self-forwarded) may not hold.
Also, check for displaced mark words when making assertions about
objects being self-forwarded.

ShenandoahHeapRegion* from_region = heap_region_containing(p);
assert(!from_region->is_humongous(), "never evacuate humongous objects");
if (has_self_forwarded_objects() && from_region->has_self_forwards()) {

@earthling-amzn earthling-amzn Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, once a thread self-forwards an object, no other threads (not even a worker) will attempt further evacuations from the region. We could defer setting this flag until after a worker has tried to evacuate all it can, but as this region will only be partially recycled anyway, it is unclear if more aggressive evacuation would be worth it.

@kdnilsen

Copy link
Copy Markdown
Contributor

Under "additional behavior changes":

  1. Should we mention that a stall that happens while old marking is running concurrently will cause the next GC trigger to request a GLOBAL GC if the stalled thread has not yet been unblocked? (or clarify if I didn't get this quite right)

  2. Can we elaborate on what is meant by "The region counter (visualizer) reports out cycles with evacuation failures as degenerated now"?

@earthling-amzn

Copy link
Copy Markdown
Contributor Author

We have:

An allocation failure during old marking will run a global collection, rather than run young and risk starving old.

I will elaborate on 2.

@kdnilsen kdnilsen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this very thorough simplification of GenShen. I like the approach. I have a few questions embedded within my comments.

_cycle_start = os::elapsedTime();
cancel_trigger_request();
log_debug(gc, ergo)("Declined trigger count at start: %zu", _declined_trigger_count.load_relaxed());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: In tip, we do not penalize if a concurrent GC "degenerates" through no fault of the triggering heuristic. We used a count of how many times we declined to trigger as an indication that the triggering heuristic might be responsible for degeneration. If it declined to trigger less than a small threshold value, then we say it was not at fault, and we do not apply a penalty.

I wonder if there's an equivalent notion now that we do not have degenerated cycles. I would expect that we apply a penalty if a GC cycle experiences allocation stalls and the scheduling heuristic had declined to trigger more than a specific threshold number of times.

I want to figure out if this mechanism is still present.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that ShenandoahHeuristics::record_concurrent_completion() still checks _declined_trigger_count before imposing a triggering penalty. I just need to find where its value is incremented.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ShenandoahHeuristics::decline_trigger() increments the count.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I tried to preserve all that machinery based on stalls.

_alloc_failure_full(0) {

Copy::zero_to_bytes(_degen_point_counts, sizeof(size_t) * ShenandoahGC::_DEGENERATED_LIMIT);
Copy::zero_to_bytes(_stall_counts, sizeof(size_t) * ShenandoahController::PHASE_LIMIT);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future, will we also report total time spent by mutator threads in stall in addition to the count of how many times threads experienced stalls?

_alloc_failure_full(0) {

Copy::zero_to_bytes(_degen_point_counts, sizeof(size_t) * ShenandoahGC::_DEGENERATED_LIMIT);
Copy::zero_to_bytes(_stall_counts, sizeof(size_t) * ShenandoahController::PHASE_LIMIT);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably would also like to see maximum time spent in any single stall on a per-thread basis.

}

oop ShenandoahGenerationalHeap::evacuate_object(oop p, Thread* thread) {
assert(thread == Thread::current(), "Expected thread parameter to be current thread.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: The GC worker threads use an object iterator and closure to evacuate every object in a region. The closure calls this method for each marked object in the region. If a GC worker thread fails to evacuate an object within its "assigned" region, the closure will continue to invoke this method for subsequent marked objects within the region. The first failed allocation will cause the heap region to be identified as having self-forwarded objects, and this will cause the subsequent invocations of this method for objects residing in the same region to also be self forwarded.

Comment thread src/hotspot/share/gc/shenandoah/shenandoahHeapRegion.cpp
Comment thread src/hotspot/share/gc/shenandoah/shenandoahHeapRegion.cpp Outdated

if (heap->is_degenerated_gc_in_progress()) {
if (heap->has_self_forwarded_objects()) {
status |= (1 << 6);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be using symbolic constants here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but can we make that a TODO? It's somewhat orthogonal to this change.

@@ -1151,7 +1145,14 @@ class ShenandoahEvacuationTask : public WorkerTask {
assert(r->has_live(), "Region %zu should have been reclaimed early", r->index());
_sh->marked_object_iterate(r, &cl);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems overly conservative. We don't know for sure that this thread introduced the self forwards. Maybe some other thread introduced the self-forwarded objects into this region.

I'm also concerned about overgeneralizing the cause for self-forwards. It is possible that the reason that OOM evac resulted in self-forwarded objects is because of a single very large object for which there is not a sufficiently large contiguous segment of memory to hold its copy, anywhere, even though there might be an abundance of memory available to hold the results of evacuating smaller objects. If that's the case, it is appropriate to self-forward that region, but it is not appropriate to shutdown this worker thread.

So there are two things I think we want to check here before shutting down this thread's efforts:

  1. Confirm that I am the thread that caused this region to have self-forwarded objects, and
  2. Confirm that the OOM during evac that I experienced is due to an inability to refresh a minimum-sized PLAB or GCLAB. If my existing GC/P LABs have at least min-size available memory and/or there is memory available to refresh my GC/P LABs, then I think we should not take this GC worker out of commission.

Willing to discuss/consider alternative perspectives, especially if we have data to contradict my recommendation here...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's fair. In general, each region will be visited by only one worker, but it's possible that some rogue mutator with an exhausted PLAB could poison a region (or regions) before the worker gets to it. I shall think on this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kdnilsen , please see this commit specifically for this feedback: b117e36

Comment thread src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp Outdated
// Note: We may have spurious wakeups here when an old gc cycle completes.
// We don't have a mechanism to wait for specific types of cycles to complete.
// In general though, nobody should be notified when an old mark increment
// completes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the "final" old-mark increment may recycle immediate garbage. In some workloads, that is fairly common (e.g. diluvian). Would you want to special case that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will think on this. In this PR I combined 'alloc waiters' (waiting for memory) and 'gc waiters' (waiting for a complete cycle) because it seemed a simplification at that the time. Note that even before this change, both such waiters could be woken up spuriously for old mark increments (even when they don't reclaim memory). I have a follow up branch based on this PR that notifies waiters when final mark frees up garbage. I'll clean this up and make a dependent PR from this one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kdnilsen - Here is a dependent PR that is meant to address your feedback: #32034. It's not trivial so I didn't want to just fold it in here. The new PR also adds a feature to unblock alloc waiters for immediate garbage even when the cycle moves on to the evacuation phase. I co-opted this old ticket: https://bugs.openjdk.org/browse/JDK-8338534.

@openjdk

openjdk Bot commented Jul 22, 2026

Copy link
Copy Markdown

⚠️ @earthling-amzn This pull request contains merges that bring in commits not present in the target repository. Since this is not a "merge style" pull request, these changes will be squashed when this pull request in integrated. If this is your intention, then please ignore this message. If you want to preserve the commit structure, you must change the title of this pull request to Merge <project>:<branch> where <project> is the name of another project in the OpenJDK organization (for example Merge jdk:master).

@openjdk openjdk Bot added the merge-conflict Pull request has merge conflict with target branch label Jul 22, 2026
@openjdk openjdk Bot removed the merge-conflict Pull request has merge conflict with target branch label Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hotspot hotspot-dev@openjdk.org hotspot-gc hotspot-gc-dev@openjdk.org rfr Pull request is ready for review shenandoah shenandoah-dev@openjdk.org

Development

Successfully merging this pull request may close these issues.

2 participants