From 66ba02bc2dfbd8a3bc0b0bca1f29c9f7bf7500fd Mon Sep 17 00:00:00 2001 From: William Kemper Date: Wed, 22 Jul 2026 16:04:55 -0700 Subject: [PATCH 1/3] WIP: decouple alloc and cycle waiters, notify appropriately --- .../gc/shenandoah/shenandoahConcurrentGC.cpp | 5 + .../gc/shenandoah/shenandoahControlThread.cpp | 78 +---------- .../gc/shenandoah/shenandoahControlThread.hpp | 19 +-- .../gc/shenandoah/shenandoahController.cpp | 75 ++++++++++- .../gc/shenandoah/shenandoahController.hpp | 22 +++- .../shenandoahGenerationalControlThread.cpp | 123 +++++------------- .../shenandoahGenerationalControlThread.hpp | 19 ++- .../share/gc/shenandoah/shenandoahHeap.cpp | 2 +- 8 files changed, 156 insertions(+), 187 deletions(-) diff --git a/src/hotspot/share/gc/shenandoah/shenandoahConcurrentGC.cpp b/src/hotspot/share/gc/shenandoah/shenandoahConcurrentGC.cpp index f4e7bcb811521..7a7bfa676fa1e 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahConcurrentGC.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahConcurrentGC.cpp @@ -219,6 +219,11 @@ bool ShenandoahConcurrentGC::collect(GCCause::Cause cause) { // This may be skipped if there is nothing to evacuate. // If so, evac_in_progress would be unset by collection set preparation code. if (heap->is_evacuation_in_progress()) { + // Final mark may have reclaimed some immediate garbage that could satisfy allocation requests. + // We are proceeding with evacuation, so notify alloc waiters (if we are not evacuating, completion + // of the abbreviated cycle would also notify them). + _controller->notify_alloc_waiters(); + // Concurrently evacuate entry_evacuate(); if (check_cancellation_and_abort()) { diff --git a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp index 3134da2af3640..5d73df27fc282 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp @@ -142,6 +142,7 @@ void ShenandoahControlThread::run_service() { // Notify waiters that a cycle is completed. They'll decide for themselves to continue waiting or not. notify_gc_waiters(); + notify_alloc_waiters(); // Report current free set state at the end of cycle, whether // it is a normal completion, or the abort. @@ -215,6 +216,7 @@ void ShenandoahControlThread::run_service() { // In case any threads are waiting for a cycle to happen, notify them so they observe the shutdown. notify_gc_waiters(); + notify_alloc_waiters(); } void ShenandoahControlThread::service_concurrent_normal_cycle(GCCause::Cause cause) { @@ -275,20 +277,7 @@ void ShenandoahControlThread::service_stw_full_cycle(GCCause::Cause cause) { gc.collect(cause); } -void ShenandoahControlThread::request_gc(GCCause::Cause cause) { - if (cause == GCCause::_shenandoah_upgrade_to_full_gc) { - handle_alloc_failure_full(); - } else if (ShenandoahCollectorPolicy::should_handle_requested_gc(cause)) { - if (ShenandoahCollectorPolicy::is_allocation_failure(cause)) { - ShenandoahHeap* heap = ShenandoahHeap::heap(); - heap->global_generation()->heuristics()->record_allocation_stall(); - heap->shenandoah_policy()->record_allocation_stall(get_phase()); - } - handle_requested_gc(cause); - } -} - -void ShenandoahControlThread::notify_control_thread(GCCause::Cause cause) { +void ShenandoahControlThread::notify_control_thread(GCCause::Cause cause, ShenandoahGeneration* ignored) { // Although setting gc request is under _controller_lock, the read side (run_service()) // does not take the lock. We need to enforce following order, so that read side sees // latest requested gc cause when the flag is set. @@ -298,63 +287,6 @@ void ShenandoahControlThread::notify_control_thread(GCCause::Cause cause) { controller.notify(); } -void ShenandoahControlThread::handle_requested_gc(GCCause::Cause cause) { - if (should_terminate()) { - log_info(gc)("Control thread is terminating, no more GCs"); - return; - } - - // For normal requested GCs (System.gc) we want to block the caller. However, - // for whitebox requested GC, we want to initiate the GC and return immediately. - // The whitebox caller thread will arrange for itself to wait until the GC notifies - // it that has reached the requested breakpoint (phase in the GC). - if (cause == GCCause::_wb_breakpoint) { - notify_control_thread(cause); - return; - } - - // Make sure we have at least one complete GC cycle before unblocking - // from the explicit GC request. - // - // This is especially important for weak references cleanup and/or native - // resources (e.g. DirectByteBuffers) machinery: when explicit GC request - // comes very late in the already running cycle, it would miss lots of new - // opportunities for cleanup that were made available before the caller - // requested the GC. - - MonitorLocker ml(&_gc_waiters_lock); - size_t current_gc_id = get_gc_id(); - size_t required_gc_id = current_gc_id + 1; - while (current_gc_id < required_gc_id && !should_terminate()) { - if (ShenandoahCollectorPolicy::is_allocation_failure(cause)) { - _alloc_stall_count.add_then_fetch(1UL); - increase_concurrent_worker_count(); - } - - notify_control_thread(cause); - ml.wait(); - current_gc_id = get_gc_id(); - if (ShenandoahCollectorPolicy::is_allocation_failure(cause)) { - break; - } - } -} - -void ShenandoahControlThread::handle_alloc_failure_full() { - if (should_terminate()) { - log_info(gc)("Control thread is terminating, no more GCs"); - return; - } - - // Make sure we have at least one full GC cycle before unblocking - // from the explicit GC request. - const ShenandoahCollectorPolicy* policy = ShenandoahHeap::heap()->shenandoah_policy(); - MonitorLocker ml(&_gc_waiters_lock); - size_t full_gc_count = policy->full_gc_count(); - const size_t required_count = full_gc_count + 1; - while (full_gc_count < required_count && !should_terminate()) { - notify_control_thread(GCCause::_shenandoah_upgrade_to_full_gc); - ml.wait(); - full_gc_count = policy->full_gc_count(); - } +ShenandoahGeneration* ShenandoahControlThread::alloc_failure_generation() { + return ShenandoahHeap::heap()->global_generation(); } diff --git a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.hpp b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.hpp index 968ffd80bcf44..c3c051960d12b 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.hpp @@ -25,15 +25,13 @@ #ifndef SHARE_GC_SHENANDOAH_SHENANDOAHCONTROLTHREAD_HPP #define SHARE_GC_SHENANDOAH_SHENANDOAHCONTROLTHREAD_HPP -#include "gc/shared/concurrentGCThread.hpp" #include "gc/shared/gcCause.hpp" #include "gc/shenandoah/shenandoahController.hpp" -#include "gc/shenandoah/shenandoahGC.hpp" -#include "gc/shenandoah/shenandoahPadding.hpp" #include "gc/shenandoah/shenandoahSharedVariables.hpp" +class ShenandoahGeneration; + class ShenandoahControlThread: public ShenandoahController { -private: typedef enum { none, concurrent_normal, @@ -49,24 +47,19 @@ class ShenandoahControlThread: public ShenandoahController { public: ShenandoahControlThread(); +protected: void run_service() override; void stop_service() override; - void request_gc(GCCause::Cause cause) override; + // Sets the requested cause, flag and notifies the control thread + void notify_control_thread(GCCause::Cause cause, ShenandoahGeneration* generation) override; + ShenandoahGeneration* alloc_failure_generation() override; private: - // Sets the requested cause and flag and notifies the control thread - void notify_control_thread(GCCause::Cause cause); - bool check_cancellation(); void service_concurrent_normal_cycle(GCCause::Cause cause); void service_stw_full_cycle(GCCause::Cause cause); - // Handle GC request. - // Blocks until GC is over. - void handle_requested_gc(GCCause::Cause cause); - - void handle_alloc_failure_full(); }; #endif // SHARE_GC_SHENANDOAH_SHENANDOAHCONTROLTHREAD_HPP diff --git a/src/hotspot/share/gc/shenandoah/shenandoahController.cpp b/src/hotspot/share/gc/shenandoah/shenandoahController.cpp index 74cd71a02d927..cc6d0c5fdff58 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahController.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahController.cpp @@ -25,6 +25,7 @@ #include "gc/shared/allocTracer.hpp" #include "gc/shared/gc_globals.hpp" +#include "gc/shenandoah/shenandoahCollectorPolicy.hpp" #include "gc/shenandoah/shenandoahController.hpp" #include "gc/shenandoah/shenandoahHeap.hpp" #include "gc/shenandoah/shenandoahHeapRegion.inline.hpp" @@ -33,6 +34,7 @@ ShenandoahController::ShenandoahController(): _gc_id(0), _phase(UNSET), _gc_waiters_lock(WAITERS_LOCK_RANK, "ShenandoahGCWaiters_lock", true), + _alloc_waiters_lock(WAITERS_LOCK_RANK, "ShenandoahAllocWaiters_lock", true), _alloc_stall_count(0), _concurrent_worker_count(ConcGCThreads) { } @@ -54,7 +56,73 @@ void ShenandoahController::handle_alloc_failure(const ShenandoahAllocRequest &re log_debug(gc)("Failed to allocate %s, " PROPERFMT, req.type_string(), PROPERFMTARGS(req_byte)); AllocTracer::send_allocation_requiring_gc_event(req_byte, checked_cast(get_gc_id())); - request_gc(cause); + ShenandoahHeap* heap = ShenandoahHeap::heap(); + alloc_failure_generation()->heuristics()->record_allocation_stall(); + heap->shenandoah_policy()->record_allocation_stall(get_phase()); + + // This is the inner part of a larger retry loop, so just wait here + MonitorLocker ml(&_alloc_waiters_lock); + _alloc_stall_count.add_then_fetch(1UL); + increase_concurrent_worker_count(); + notify_control_thread(cause, alloc_failure_generation()); + ml.wait(); +} + +void ShenandoahController::handle_alloc_failure_full() { + if (should_terminate()) { + log_info(gc)("Control thread is terminating, no more GCs"); + return; + } + + // Make sure we have at least one full GC cycle before unblocking from the explicit GC request. + ShenandoahHeap* heap = ShenandoahHeap::heap(); + const ShenandoahCollectorPolicy* policy = heap->shenandoah_policy(); + MonitorLocker ml(&_gc_waiters_lock); + size_t full_gc_count = policy->full_gc_count(); + const size_t required_count = full_gc_count + 1; + while (full_gc_count < required_count && !should_terminate()) { + notify_control_thread(GCCause::_shenandoah_upgrade_to_full_gc, heap->global_generation()); + ml.wait(); + full_gc_count = policy->full_gc_count(); + } +} + +void ShenandoahController::request_gc(GCCause::Cause cause) { + if (ShenandoahCollectorPolicy::should_handle_requested_gc(cause)) { + handle_requested_gc(cause); + } +} + +void ShenandoahController::handle_requested_gc(GCCause::Cause cause) { + + // Requested gc's always operate on the entire heap + ShenandoahGeneration* global = ShenandoahHeap::heap()->global_generation(); + + // For normal requested GCs (System.gc) we want to block the caller. However, + // for whitebox requested GC, we want to initiate the GC and return immediately. + // The whitebox caller thread will arrange for itself to wait until the GC notifies + // it that has reached the requested breakpoint (phase in the GC). + if (cause == GCCause::_wb_breakpoint) { + notify_control_thread(cause, global); + return; + } + + // Make sure we have at least one complete GC cycle before unblocking + // from the explicit GC request. + // + // This is especially important for weak references cleanup and/or native + // resources (e.g. DirectByteBuffers) machinery: when explicit GC request + // comes very late in the already running cycle, it would miss lots of new + // opportunities for cleanup that were made available before the caller + // requested the GC. + MonitorLocker ml(&_gc_waiters_lock); + size_t current_gc_id = get_gc_id(); + size_t required_gc_id = current_gc_id + 1; + while (current_gc_id < required_gc_id && !should_terminate()) { + notify_control_thread(cause, global); + ml.wait(); + current_gc_id = get_gc_id(); + } } void ShenandoahController::increase_concurrent_worker_count() { @@ -93,6 +161,11 @@ void ShenandoahController::notify_gc_waiters() { ml.notify_all(); } +void ShenandoahController::notify_alloc_waiters() { + MonitorLocker ml(&_alloc_waiters_lock); + ml.notify_all(); +} + const char* ShenandoahController::collector_phase_to_string(ShenandoahCollectorPhase phase) { switch(phase) { case UNSET: return "Outside of Cycle"; diff --git a/src/hotspot/share/gc/shenandoah/shenandoahController.hpp b/src/hotspot/share/gc/shenandoah/shenandoahController.hpp index c426f1e3b815e..5ad8c376063c0 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahController.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahController.hpp @@ -31,6 +31,8 @@ #include "gc/shenandoah/shenandoahPadding.hpp" #include "runtime/atomic.hpp" +class ShenandoahGeneration; + /** * This interface exposes methods necessary for the heap to interact * with the threads responsible for driving the collection cycle. @@ -60,8 +62,14 @@ class ShenandoahController: public ConcurrentGCThread { const Mutex::Rank WAITERS_LOCK_RANK = Mutex::safepoint - 5; const Mutex::Rank CONTROL_LOCK_RANK = Mutex::nosafepoint - 2; + // Threads waiting for a complete gc cycle (full gc or concurrent global) will wait here. Monitor _gc_waiters_lock; + // Threads waiting for the gc to free memory will wait here. Note that immediate garabge may be reclaimed + // midcycle during final mark, so we want to notify alloc waiters independently of cycle waiters. Similarly, + // no threads should be notified when a concurrent old mark increment is interrupted. + Monitor _alloc_waiters_lock; + // The number of stalls experienced during the cycle. Incremented by mutators, reset by control thread. shenandoah_padding(2); Atomic _alloc_stall_count; @@ -84,19 +92,26 @@ class ShenandoahController: public ConcurrentGCThread { return _alloc_stall_count.load_relaxed(); } + virtual ShenandoahGeneration* alloc_failure_generation() = 0; + virtual void notify_control_thread(GCCause::Cause cause, ShenandoahGeneration* generation) = 0; + public: ShenandoahController(); // Request a collection cycle. This handles "explicit" gc requests // like System.gc and "implicit" gc requests, like metaspace oom. - virtual void request_gc(GCCause::Cause cause) = 0; + virtual void request_gc(GCCause::Cause cause); - // Notify threads that the gc has reclaimed memory (or that it completed a cycle, or it's exiting). + // Notify threads that the gc has completed a cycle, or it's exiting. void notify_gc_waiters(); + // Notify threads that the gc has recovered memory, or it's exiting. + void notify_alloc_waiters(); + // This cancels the collection cycle and has an option to block // until another cycle completes successfully. void handle_alloc_failure(const ShenandoahAllocRequest &req); + void handle_alloc_failure_full(); // Return suggested number of concurrent worker threads size_t concurrent_worker_count() const { @@ -117,5 +132,8 @@ class ShenandoahController: public ConcurrentGCThread { } static const char* collector_phase_to_string(ShenandoahCollectorPhase phase); + +protected: + void handle_requested_gc(GCCause::Cause cause); }; #endif // SHARE_GC_SHENANDOAH_SHENANDOAHCONTROLLER_HPP diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp index 3e9c862e10f17..25b9c1c379b15 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp @@ -57,6 +57,24 @@ ShenandoahGenerationalControlThread::ShenandoahGenerationalControlThread() : create_and_start(); } +void ShenandoahGenerationalControlThread::request_gc(GCCause::Cause cause) { + if (ShenandoahCollectorPolicy::should_handle_requested_gc(cause)) { + + if (gc_mode() == servicing_old) { + MonitorLocker control_locker(&_control_lock, Mutex::_no_safepoint_check_flag); + if (gc_mode() == servicing_old) { + _heap->cancel_gc(cause); + + // TODO: Do we really even need to notify control thread here? + // We just need the workers to observe the cancellation. + notify_control_thread(control_locker, cause, _heap->global_generation()); + } + } + + handle_requested_gc(cause); + } +} + void ShenandoahGenerationalControlThread::run_service() { // This is the only instance of request. @@ -86,6 +104,7 @@ void ShenandoahGenerationalControlThread::run_service() { // In case any threads are waiting for a cycle to happen, notify them so they observe the shutdown. notify_gc_waiters(); + notify_alloc_waiters(); set_gc_mode(stopped); } @@ -98,6 +117,10 @@ void ShenandoahGenerationalControlThread::stop_service() { // to reach a safepoint (this runs on a java thread). } +ShenandoahGeneration* ShenandoahGenerationalControlThread::alloc_failure_generation() { + return _heap->young_generation(); +} + bool ShenandoahGenerationalControlThread::should_run_full_gc(GCCause::Cause cause) const { const bool old_evacs_failed = _heap->old_generation()->has_failed_evacuations(); return old_evacs_failed || ShenandoahCollectorPolicy::should_run_full_gc(cause); @@ -274,9 +297,6 @@ void ShenandoahGenerationalControlThread::run_gc_cycle(const ShenandoahGCRequest // Try to reduce concurrent workers decrease_concurrent_worker_count(); - // Notify threads waiting for GC - notify_gc_waiters(); - _heap->free_set()->log_status_under_lock(); // Notify Universe about new heap usage. This has implications for @@ -431,6 +451,12 @@ void ShenandoahGenerationalControlThread::resume_concurrent_old_cycle(Shenandoah _heap->notify_gc_progress(); generation->heuristics()->record_concurrent_completion(); _heap->shenandoah_policy()->record_success_old(); + + // Old marking has completed, it will have rebuilt the free set with any immediate garbage. + // Notify alloc waiters. We don't notify cycle waiters because no threads should be waiting + // for an old cycle and because cycle waiters would have requested a global cycle that would + // have cancelled the old mark anyway. + notify_alloc_waiters(); } if (_heap->cancelled_gc() && cause == GCCause::_shenandoah_concurrent_gc) { @@ -473,6 +499,10 @@ void ShenandoahGenerationalControlThread::service_concurrent_cycle(ShenandoahGen // direct heuristic signals to the young heuristics _heap->young_generation()->heuristics()->record_concurrent_completion(); _heap->shenandoah_policy()->record_success_concurrent(generation->is_young(), gc.abbreviated()); + + // Notify threads waiting for GC and/or memory when a young or global cycle completes + notify_gc_waiters(); + notify_alloc_waiters(); } else { assert(_heap->cancelled_gc(), "Must have been cancelled"); } @@ -514,20 +544,10 @@ void ShenandoahGenerationalControlThread::service_stw_full_cycle(GCCause::Cause ShenandoahGCSession session(cause, _heap->global_generation()); ShenandoahFullGC gc; gc.collect(cause); -} -void ShenandoahGenerationalControlThread::request_gc(GCCause::Cause cause) { - if (cause == GCCause::_shenandoah_upgrade_to_full_gc) { - handle_alloc_failure_full(); - } else if (ShenandoahCollectorPolicy::is_allocation_failure(cause)) { - ShenandoahHeap* heap = ShenandoahHeap::heap(); - ShenandoahGeneration* young = heap->young_generation(); - young->heuristics()->record_allocation_stall(); - heap->shenandoah_policy()->record_allocation_stall(get_phase()); - handle_requested_gc(cause, young); - } else if (ShenandoahCollectorPolicy::should_handle_requested_gc(cause)) { - handle_requested_gc(cause, ShenandoahHeap::heap()->global_generation()); - } + // Notify threads waiting for GC and/or memory that a full cycle has completed + notify_gc_waiters(); + notify_alloc_waiters(); } bool ShenandoahGenerationalControlThread::request_concurrent_gc(ShenandoahGeneration* generation) { @@ -615,77 +635,6 @@ bool ShenandoahGenerationalControlThread::preempt_old_marking(ShenandoahGenerati return generation->is_young() && _allow_old_preemption.try_unset(); } -void ShenandoahGenerationalControlThread::handle_requested_gc(GCCause::Cause cause, ShenandoahGeneration* generation) { - // For normal requested GCs (System.gc) we want to block the caller. However, - // for whitebox requested GC, we want to initiate the GC and return immediately. - // The whitebox caller thread will arrange for itself to wait until the GC notifies - // it that has reached the requested breakpoint (phase in the GC). - if (cause == GCCause::_wb_breakpoint) { - notify_control_thread(cause, ShenandoahHeap::heap()->global_generation()); - return; - } - - if (gc_mode() == servicing_old) { - MonitorLocker control_locker(&_control_lock, Mutex::_no_safepoint_check_flag); - if (gc_mode() == servicing_old) { - _heap->cancel_gc(cause); - notify_control_thread(control_locker, cause, generation); - } - } - - // Make sure we have at least one complete GC cycle before unblocking - // from the explicit GC request. - // - // This is especially important for weak references cleanup and/or native - // resources (e.g. DirectByteBuffers) machinery: when explicit GC request - // comes very late in the already running cycle, it would miss lots of new - // opportunities for cleanup that were made available before the caller - // requested the GC. - // - // 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. - MonitorLocker ml(&_gc_waiters_lock); - size_t current_gc_id = get_gc_id(); - const size_t required_gc_id = current_gc_id + 1; - while (current_gc_id < required_gc_id && !should_terminate()) { - if (ShenandoahCollectorPolicy::is_allocation_failure(cause)) { - _alloc_stall_count.add_then_fetch(1UL); - increase_concurrent_worker_count(); - log_debug(gc, thread)("Alloc stall count now: %zu", alloc_stall_count()); - } - - // Make requests to run a cycle until at least one is completed - notify_control_thread(cause, generation); - ml.wait(); - current_gc_id = get_gc_id(); - if (ShenandoahCollectorPolicy::is_allocation_failure(cause)) { - break; - } - } -} - -void ShenandoahGenerationalControlThread::handle_alloc_failure_full() { - if (should_terminate()) { - log_info(gc)("Control thread is terminating, no more GCs"); - return; - } - - // Make sure we have at least one full GC cycle before unblocking - // from the explicit GC request. - const ShenandoahHeap* heap = ShenandoahHeap::heap(); - const ShenandoahCollectorPolicy* policy = heap->shenandoah_policy(); - MonitorLocker ml(&_gc_waiters_lock); - size_t full_gc_count = policy->full_gc_count(); - const size_t required_count = full_gc_count + 1; - while (full_gc_count < required_count && !should_terminate()) { - notify_control_thread(GCCause::_shenandoah_upgrade_to_full_gc, heap->global_generation()); - ml.wait(); - full_gc_count = policy->full_gc_count(); - } -} - const char* ShenandoahGenerationalControlThread::gc_mode_name(GCMode mode) { switch (mode) { case none: return "idle"; diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp index c1e57664c14d0..25e027b43123f 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp @@ -81,9 +81,7 @@ class ShenandoahGenerationalControlThread: public ShenandoahController { public: ShenandoahGenerationalControlThread(); - void run_service() override; - void stop_service() override; - + // Handles explict requests. Overridden to deal with cancelling old if necessary. void request_gc(GCCause::Cause cause) override; // Return true if the request to start a concurrent GC for the given generation succeeded. @@ -93,6 +91,14 @@ class ShenandoahGenerationalControlThread: public ShenandoahController { GCMode gc_mode() const { return _gc_mode; } + +protected: + void run_service() override; + void stop_service() override; + + ShenandoahGeneration* alloc_failure_generation() override; + void notify_control_thread(GCCause::Cause cause, ShenandoahGeneration* generation) override; + private: // Executes one GC cycle @@ -107,12 +113,6 @@ class ShenandoahGenerationalControlThread: public ShenandoahController { void service_concurrent_normal_cycle(const ShenandoahGCRequest& request); void service_concurrent_old_cycle(const ShenandoahGCRequest& request); - // Blocks until at least one cycle is complete. WARNING: it doesn't know what kind of cycle will be run. - void handle_requested_gc(GCCause::Cause cause, ShenandoahGeneration* generation); - - // Blocks until at least one full GC cycle is comp - void handle_alloc_failure_full(); - // Returns true if the old generation marking was interrupted to allow a young cycle. bool preempt_old_marking(ShenandoahGeneration* generation); @@ -128,7 +128,6 @@ class ShenandoahGenerationalControlThread: public ShenandoahController { // Updating the requested generation is not necessary for allocation failures nor when stopping the thread. void notify_control_thread(GCCause::Cause cause); void notify_control_thread(MonitorLocker& ml, GCCause::Cause cause); - void notify_control_thread(GCCause::Cause cause, ShenandoahGeneration* generation); void notify_control_thread(MonitorLocker& ml, GCCause::Cause cause, ShenandoahGeneration* generation); // Take the _control_lock and check for a request to run a gc cycle. If a request is found, diff --git a/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp b/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp index 8518c34041ee7..933293e9ac807 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp @@ -983,7 +983,7 @@ HeapWord* ShenandoahHeap::allocate_memory(ShenandoahAllocRequest& req) { if (current_count - original_count > ShenandoahFullGCThreshold) { // We are not getting what we need from concurrent allocations, so request a full gc. // Whether this satisfies the allocation or not, we are done trying. - control_thread()->request_gc(GCCause::_shenandoah_upgrade_to_full_gc); + control_thread()->handle_alloc_failure_full(); result = allocate_memory_work(req, in_new_region); break; } From 863f69f94ac01f5562fd4a6c44f0e5a14dd116fd Mon Sep 17 00:00:00 2001 From: William Kemper Date: Thu, 23 Jul 2026 21:33:21 +0000 Subject: [PATCH 2/3] WIP: Hold alloc waiter lock when resetting request --- .../gc/shenandoah/shenandoahController.cpp | 4 ++- .../shenandoahGenerationalControlThread.cpp | 30 +++++++++++-------- .../shenandoahGenerationalControlThread.hpp | 2 ++ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/hotspot/share/gc/shenandoah/shenandoahController.cpp b/src/hotspot/share/gc/shenandoah/shenandoahController.cpp index cc6d0c5fdff58..502ddf82435a9 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahController.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahController.cpp @@ -65,7 +65,9 @@ void ShenandoahController::handle_alloc_failure(const ShenandoahAllocRequest &re _alloc_stall_count.add_then_fetch(1UL); increase_concurrent_worker_count(); notify_control_thread(cause, alloc_failure_generation()); - ml.wait(); + if (!should_terminate()) { + ml.wait(); + } } void ShenandoahController::handle_alloc_failure_full() { diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp index 25b9c1c379b15..d884d005801a5 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp @@ -226,6 +226,22 @@ void ShenandoahGenerationalControlThread::maybe_print_young_region_ages() const } } +void ShenandoahGenerationalControlThread::clear_allocation_failure_and_notify_waiters() { + MonitorLocker waiters(&_alloc_waiters_lock, Mutex::_no_safepoint_check_flag); + { + MonitorLocker ml(&_control_lock, Mutex::_no_safepoint_check_flag); + if (ShenandoahCollectorPolicy::is_allocation_failure(_requested_gc_cause)) { + // If an allocation failure occurred during this cycle, we'll have threads waiting + // for reclaimed memory. We'll wake them up, and they'll retry their allocation. + // If our waiters cannot allocate, they will signal the control thread again + // to start another cycle. + _heap->clear_cancellation(_requested_gc_cause); + _requested_gc_cause = GCCause::_no_gc; + } + } + waiters.notify_all(); +} + void ShenandoahGenerationalControlThread::run_gc_cycle(const ShenandoahGCRequest& request) { log_debug(gc, thread)("Starting GC (%s): %s, %s", gc_mode_name(gc_mode()), GCCause::to_string(request.cause), request.generation->name()); @@ -269,11 +285,13 @@ void ShenandoahGenerationalControlThread::run_gc_cycle(const ShenandoahGCRequest service_concurrent_old_cycle(request); } else { service_concurrent_normal_cycle(request); + clear_allocation_failure_and_notify_waiters(); } break; } case stw_full: { service_stw_full_cycle(request.cause); + clear_allocation_failure_and_notify_waiters(); break; } default: @@ -282,18 +300,6 @@ void ShenandoahGenerationalControlThread::run_gc_cycle(const ShenandoahGCRequest _heap->print_after_gc(); } - { - MonitorLocker ml(&_control_lock, Mutex::_no_safepoint_check_flag); - if (ShenandoahCollectorPolicy::is_allocation_failure(_requested_gc_cause)) { - // If an allocation failure occurred during this cycle, we'll have threads waiting - // to reclaim memory. We'll wake them up, and they'll retry their allocation. - // If our waiters cannot allocate, they will signal the control thread again - // to start another cycle. - _heap->clear_cancellation(_requested_gc_cause); - _requested_gc_cause = GCCause::_no_gc; - } - } - // Try to reduce concurrent workers decrease_concurrent_worker_count(); diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp index 25e027b43123f..ecc4dde757b35 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp @@ -141,6 +141,8 @@ class ShenandoahGenerationalControlThread: public ShenandoahController { // Print table for young region ages if log is enabled void maybe_print_young_region_ages() const; + void clear_allocation_failure_and_notify_waiters(); + // Returns true if we should run a full gc bool should_run_full_gc(GCCause::Cause cause) const; }; From 1b23c7538026f25afa926273199bda731fda102e Mon Sep 17 00:00:00 2001 From: William Kemper Date: Thu, 23 Jul 2026 15:57:28 -0700 Subject: [PATCH 3/3] Cleanup --- .../gc/shenandoah/shenandoahControlThread.cpp | 11 ++++- .../gc/shenandoah/shenandoahControlThread.hpp | 2 +- .../gc/shenandoah/shenandoahController.cpp | 7 +--- .../gc/shenandoah/shenandoahController.hpp | 12 ++++-- .../shenandoahGenerationalControlThread.cpp | 41 +++++++++++-------- .../shenandoahGenerationalControlThread.hpp | 5 ++- 6 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp index 5d73df27fc282..0c777db05369f 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp @@ -287,6 +287,13 @@ void ShenandoahControlThread::notify_control_thread(GCCause::Cause cause, Shenan controller.notify(); } -ShenandoahGeneration* ShenandoahControlThread::alloc_failure_generation() { - return ShenandoahHeap::heap()->global_generation(); +void ShenandoahControlThread::notify_alloc_stall(GCCause::Cause cause) { + ShenandoahHeap* heap = ShenandoahHeap::heap(); + ShenandoahGeneration* generation = heap->global_generation(); + + // Inform global heuristics of allocation stall + generation->heuristics()->record_allocation_stall(); + + // Run a global cycle (no other kind for this control thread). + notify_control_thread(cause, generation); } diff --git a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.hpp b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.hpp index c3c051960d12b..06582dca159c1 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.hpp @@ -53,7 +53,7 @@ class ShenandoahControlThread: public ShenandoahController { // Sets the requested cause, flag and notifies the control thread void notify_control_thread(GCCause::Cause cause, ShenandoahGeneration* generation) override; - ShenandoahGeneration* alloc_failure_generation() override; + void notify_alloc_stall(GCCause::Cause cause) override; private: bool check_cancellation(); diff --git a/src/hotspot/share/gc/shenandoah/shenandoahController.cpp b/src/hotspot/share/gc/shenandoah/shenandoahController.cpp index 502ddf82435a9..82dcddbd37510 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahController.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahController.cpp @@ -56,15 +56,12 @@ void ShenandoahController::handle_alloc_failure(const ShenandoahAllocRequest &re log_debug(gc)("Failed to allocate %s, " PROPERFMT, req.type_string(), PROPERFMTARGS(req_byte)); AllocTracer::send_allocation_requiring_gc_event(req_byte, checked_cast(get_gc_id())); - ShenandoahHeap* heap = ShenandoahHeap::heap(); - alloc_failure_generation()->heuristics()->record_allocation_stall(); - heap->shenandoah_policy()->record_allocation_stall(get_phase()); - // This is the inner part of a larger retry loop, so just wait here MonitorLocker ml(&_alloc_waiters_lock); _alloc_stall_count.add_then_fetch(1UL); increase_concurrent_worker_count(); - notify_control_thread(cause, alloc_failure_generation()); + ShenandoahHeap::heap()->shenandoah_policy()->record_allocation_stall(get_phase()); + notify_alloc_stall(cause); if (!should_terminate()) { ml.wait(); } diff --git a/src/hotspot/share/gc/shenandoah/shenandoahController.hpp b/src/hotspot/share/gc/shenandoah/shenandoahController.hpp index 5ad8c376063c0..19b27abde4fd3 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahController.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahController.hpp @@ -92,9 +92,13 @@ class ShenandoahController: public ConcurrentGCThread { return _alloc_stall_count.load_relaxed(); } - virtual ShenandoahGeneration* alloc_failure_generation() = 0; + // Notify the control thread to run a cycle for the given generation virtual void notify_control_thread(GCCause::Cause cause, ShenandoahGeneration* generation) = 0; + // Notify the control thread about an allocation stall specifically. Implementation + // will decide which generation to use and any other mode specific work that must be done. + virtual void notify_alloc_stall(GCCause::Cause cause) = 0; + public: ShenandoahController(); @@ -108,9 +112,10 @@ class ShenandoahController: public ConcurrentGCThread { // Notify threads that the gc has recovered memory, or it's exiting. void notify_alloc_waiters(); - // This cancels the collection cycle and has an option to block - // until another cycle completes successfully. + // Inform the control thread that this allocation request failed. Caller will block until memory is reclaimed. void handle_alloc_failure(const ShenandoahAllocRequest &req); + + // Run a full GC, callers will block until at least one full GC cycle is completed. void handle_alloc_failure_full(); // Return suggested number of concurrent worker threads @@ -125,6 +130,7 @@ class ShenandoahController: public ConcurrentGCThread { return _phase.load_relaxed(); } + // Record the current phase for the cycle. Used to track where allocation stalls occur. void set_phase(ShenandoahCollectorPhase phase) { assert(ShenandoahCollectorPhase::UNSET <= phase, "Phase out of bounds: %d", phase); assert(phase < ShenandoahCollectorPhase::PHASE_LIMIT, "Phase out of bounds %d", phase); diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp index d884d005801a5..b33f192e9d71b 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp @@ -57,20 +57,18 @@ ShenandoahGenerationalControlThread::ShenandoahGenerationalControlThread() : create_and_start(); } -void ShenandoahGenerationalControlThread::request_gc(GCCause::Cause cause) { - if (ShenandoahCollectorPolicy::should_handle_requested_gc(cause)) { - +void ShenandoahGenerationalControlThread::maybe_cancel_old_cycle(GCCause::Cause cause) { + if (gc_mode() == servicing_old) { + MonitorLocker control_locker(&_control_lock, Mutex::_no_safepoint_check_flag); if (gc_mode() == servicing_old) { - MonitorLocker control_locker(&_control_lock, Mutex::_no_safepoint_check_flag); - if (gc_mode() == servicing_old) { - _heap->cancel_gc(cause); - - // TODO: Do we really even need to notify control thread here? - // We just need the workers to observe the cancellation. - notify_control_thread(control_locker, cause, _heap->global_generation()); - } + _heap->cancel_gc(cause); } + } +} +void ShenandoahGenerationalControlThread::request_gc(GCCause::Cause cause) { + if (ShenandoahCollectorPolicy::should_handle_requested_gc(cause)) { + maybe_cancel_old_cycle(cause); handle_requested_gc(cause); } } @@ -117,8 +115,17 @@ void ShenandoahGenerationalControlThread::stop_service() { // to reach a safepoint (this runs on a java thread). } -ShenandoahGeneration* ShenandoahGenerationalControlThread::alloc_failure_generation() { - return _heap->young_generation(); +void ShenandoahGenerationalControlThread::notify_alloc_stall(GCCause::Cause cause) { + ShenandoahGeneration* generation = _heap->young_generation(); + + // Tell the young generation heuristics about the stall. + generation->heuristics()->record_allocation_stall(); + + // If an old mark increment is running, set cancellation flag for workers to observe. + maybe_cancel_old_cycle(cause); + + // Finally, notify the control thread to wake up and run + notify_control_thread(cause, generation); } bool ShenandoahGenerationalControlThread::should_run_full_gc(GCCause::Cause cause) const { @@ -285,13 +292,11 @@ void ShenandoahGenerationalControlThread::run_gc_cycle(const ShenandoahGCRequest service_concurrent_old_cycle(request); } else { service_concurrent_normal_cycle(request); - clear_allocation_failure_and_notify_waiters(); } break; } case stw_full: { service_stw_full_cycle(request.cause); - clear_allocation_failure_and_notify_waiters(); break; } default: @@ -462,7 +467,7 @@ void ShenandoahGenerationalControlThread::resume_concurrent_old_cycle(Shenandoah // Notify alloc waiters. We don't notify cycle waiters because no threads should be waiting // for an old cycle and because cycle waiters would have requested a global cycle that would // have cancelled the old mark anyway. - notify_alloc_waiters(); + clear_allocation_failure_and_notify_waiters(); } if (_heap->cancelled_gc() && cause == GCCause::_shenandoah_concurrent_gc) { @@ -508,7 +513,7 @@ void ShenandoahGenerationalControlThread::service_concurrent_cycle(ShenandoahGen // Notify threads waiting for GC and/or memory when a young or global cycle completes notify_gc_waiters(); - notify_alloc_waiters(); + clear_allocation_failure_and_notify_waiters(); } else { assert(_heap->cancelled_gc(), "Must have been cancelled"); } @@ -553,7 +558,7 @@ void ShenandoahGenerationalControlThread::service_stw_full_cycle(GCCause::Cause // Notify threads waiting for GC and/or memory that a full cycle has completed notify_gc_waiters(); - notify_alloc_waiters(); + clear_allocation_failure_and_notify_waiters(); } bool ShenandoahGenerationalControlThread::request_concurrent_gc(ShenandoahGeneration* generation) { diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp index ecc4dde757b35..8272ab49af98e 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp @@ -81,6 +81,9 @@ class ShenandoahGenerationalControlThread: public ShenandoahController { public: ShenandoahGenerationalControlThread(); + // Cancels gc if old mark is in progress + void maybe_cancel_old_cycle(GCCause::Cause cause); + // Handles explict requests. Overridden to deal with cancelling old if necessary. void request_gc(GCCause::Cause cause) override; @@ -96,7 +99,7 @@ class ShenandoahGenerationalControlThread: public ShenandoahController { void run_service() override; void stop_service() override; - ShenandoahGeneration* alloc_failure_generation() override; + void notify_alloc_stall(GCCause::Cause cause) override; void notify_control_thread(GCCause::Cause cause, ShenandoahGeneration* generation) override; private: