diff --git a/src/hotspot/share/gc/g1/g1Allocator.cpp b/src/hotspot/share/gc/g1/g1Allocator.cpp index 78710084ee37e..f54611e32fdb9 100644 --- a/src/hotspot/share/gc/g1/g1Allocator.cpp +++ b/src/hotspot/share/gc/g1/g1Allocator.cpp @@ -35,6 +35,7 @@ #include "gc/g1/g1Policy.hpp" #include "gc/shared/tlab_globals.hpp" #include "runtime/mutexLocker.hpp" +#include "runtime/safepoint.hpp" #include "utilities/align.hpp" G1Allocator::G1Allocator(G1CollectedHeap* heap) : @@ -210,7 +211,8 @@ size_t G1Allocator::unsafe_max_tlab_alloc() { } size_t G1Allocator::used_in_alloc_regions() { - assert(Heap_lock->owner() != nullptr, "Should be owned on this thread's behalf."); + assert(Heap_lock->owner() != nullptr || SafepointSynchronize::is_at_safepoint(), + "Should be owned on this thread's behalf or at safepoint."); size_t used = 0; for (uint i = 0; i < _num_alloc_regions; i++) { used += mutator_alloc_region(i)->used_in_alloc_regions(); diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp index be9ecf191236e..1105d1f63d1ce 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp @@ -43,6 +43,7 @@ #include "gc/g1/g1GCCounters.hpp" #include "gc/g1/g1GCParPhaseTimesTracker.hpp" #include "gc/g1/g1GCPhaseTimes.hpp" +#include "gc/g1/g1HeapEvaluationTask.hpp" #include "gc/g1/g1HeapRegion.inline.hpp" #include "gc/g1/g1HeapRegionPrinter.hpp" #include "gc/g1/g1HeapRegionRemSet.inline.hpp" @@ -109,6 +110,7 @@ #include "runtime/orderAccess.hpp" #include "runtime/threads.hpp" #include "runtime/threadSMR.hpp" +#include "runtime/vmOperations.hpp" #include "runtime/vmThread.hpp" #include "utilities/align.hpp" #include "utilities/autoRestore.hpp" @@ -873,6 +875,11 @@ void G1CollectedHeap::prepare_for_mutator_after_full_collection(size_t allocatio start_new_collection_set(); _allocator->init_mutator_alloc_regions(); + // Reset timestamps for time-based heap sizing + if (G1UseTimeBasedHeapSizing) { + _hrm.reset_free_region_timestamps(); + } + // Post collection state updates. MetaspaceGC::compute_new_size(); } @@ -1183,6 +1190,73 @@ bool G1CollectedHeap::expand_single_region(uint node_index) { return true; } +void G1CollectedHeap::shrink_with_time_based_selection(size_t shrink_bytes) { + if (capacity() == min_capacity()) { + log_debug(gc, ergo, heap)("Time-based shrink: Did not shrink the heap (heap already at minimum)"); + return; + } + + size_t aligned_shrink_bytes = os::align_down_vm_page_size(shrink_bytes); + aligned_shrink_bytes = align_down(aligned_shrink_bytes, G1HeapRegion::GrainBytes); + + aligned_shrink_bytes = capacity() - MAX2(capacity() - aligned_shrink_bytes, min_capacity()); + assert(is_aligned(aligned_shrink_bytes, G1HeapRegion::GrainBytes), "Bytes to shrink %zuB not aligned", aligned_shrink_bytes); + + log_debug(gc, ergo, heap)("Time-based shrink: Requested shrink amount: %zuB aligned shrink amount: %zuB", + shrink_bytes, aligned_shrink_bytes); + + if (aligned_shrink_bytes == 0) { + log_debug(gc, ergo, heap)("Time-based shrink: Did not shrink the heap (shrink request too small)"); + return; + } + + _verifier->verify_region_sets_optional(); + + // We should only reach here from the service thread during idle time. + // Note: Unlike full GC shrinking, time-based shrink may have an active mutator alloc region. + // This is safe because we only remove free regions, not allocated ones. + assert(GCCause::is_user_requested_gc(gc_cause()) || gc_cause() == GCCause::_no_gc, + "unexpected GC cause: %s", GCCause::to_string(gc_cause())); + + // For time-based shrink, we use time-aware selection instead of removing from end. + _hrm.remove_all_free_regions(); + shrink_helper_with_time_based_selection(aligned_shrink_bytes); + rebuild_region_sets(true /* free_list_only */); + + _hrm.verify_optional(); + _verifier->verify_region_sets_optional(); +} + +void G1CollectedHeap::shrink_helper_with_time_based_selection(size_t shrink_bytes) { + assert(shrink_bytes > 0, "must be"); + assert(is_aligned(shrink_bytes, G1HeapRegion::GrainBytes), + "Shrink request for %zuB not aligned to heap region size %zuB", + shrink_bytes, G1HeapRegion::GrainBytes); + + uint num_regions_to_remove = (uint)(shrink_bytes / G1HeapRegion::GrainBytes); + uint num_regions_removed = 0; + + // Use time-based selection to shrink oldest eligible regions + log_debug(gc, ergo, heap)("Time-based shrink: removing %u oldest regions (%zuB)", + num_regions_to_remove, shrink_bytes); + num_regions_removed = _hrm.shrink_by(num_regions_to_remove, true /* use_time_based_selection */); + + size_t shrunk_bytes = num_regions_removed * G1HeapRegion::GrainBytes; + log_debug(gc, ergo, heap)("Time-based shrink: Requested shrinking amount: %zuB actual shrinking amount: %zuB (%u regions)", + shrink_bytes, shrunk_bytes, num_regions_removed); + + if (num_regions_removed > 0) { + log_info(gc, heap)("Time-based shrink: uncommitted %u oldest regions (%zuMB), heap size now %zuMB", + num_regions_removed, shrunk_bytes / M, capacity() / M); + log_debug(gc, heap)("Time-based shrink details: requested=%zuB actual=%zuB " + "regions_removed=%u heap_capacity=%zuB", + shrink_bytes, shrunk_bytes, num_regions_removed, capacity()); + policy()->record_new_heap_size(num_committed_regions()); + } else { + log_debug(gc, ergo, heap)("Time-based shrink: Did not shrink the heap (no eligible regions found)"); + } +} + void G1CollectedHeap::shrink_helper(size_t shrink_bytes) { assert(shrink_bytes > 0, "must be"); assert(is_aligned(shrink_bytes, G1HeapRegion::GrainBytes), @@ -1190,13 +1264,24 @@ void G1CollectedHeap::shrink_helper(size_t shrink_bytes) { shrink_bytes, G1HeapRegion::GrainBytes); uint num_regions_to_remove = (uint)(shrink_bytes / G1HeapRegion::GrainBytes); + uint num_regions_removed = 0; + + // Always perform normal heap shrinking when requested + // This preserves the original GC-triggered shrinking behavior + num_regions_removed = _hrm.shrink_by(num_regions_to_remove); - uint num_regions_removed = _hrm.shrink_by(num_regions_to_remove); size_t shrunk_bytes = num_regions_removed * G1HeapRegion::GrainBytes; - log_debug(gc, ergo, heap)("Heap resize. Requested shrinking amount: %zuB actual shrinking amount: %zuB (%u regions)", - shrink_bytes, shrunk_bytes, num_regions_removed); if (num_regions_removed > 0) { + if (log_is_enabled(Debug, gc, heap)) { + log_debug(gc, heap)("Heap shrink: uncommitted %u regions (%zuMB), heap size now %zuMB. " + "Details: requested=%zuB actual=%zuB heap_capacity=%zuB", + num_regions_removed, shrunk_bytes / M, capacity() / M, + shrink_bytes, shrunk_bytes, capacity()); + } else { + log_info(gc, heap)("Heap shrink: uncommitted %u regions (%zuMB), heap size now %zuMB", + num_regions_removed, shrunk_bytes / M, capacity() / M); + } policy()->record_new_heap_size(num_committed_regions()); } else { log_debug(gc, ergo, heap)("Heap resize. Did not shrink the heap (heap shrinking operation failed)"); @@ -1241,6 +1326,20 @@ void G1CollectedHeap::shrink(size_t shrink_bytes) { _verifier->verify_region_sets_optional(); } +void G1CollectedHeap::request_heap_shrink(size_t shrink_bytes) { + if (shrink_bytes == 0) { + return; + } + + // Capture GC count before scheduling to detect if a GC occurs in the interim. + uint gc_count_before = total_collections(); + + // Always schedule a VM operation for proper synchronization with GC. + // The VM operation will re-evaluate which regions to uncommit at the time of execution. + VM_G1ShrinkHeap op(this, gc_count_before, shrink_bytes); + VMThread::execute(&op); +} + class OldRegionSetChecker : public G1HeapRegionSetChecker { public: void check_mt_safety() { @@ -1310,6 +1409,7 @@ G1CollectedHeap::G1CollectedHeap() : _old_set("Old Region Set", new OldRegionSetChecker()), _humongous_set("Humongous Region Set", new HumongousRegionSetChecker()), _bot(nullptr), + _heap_evaluation_task(nullptr), _listener(), _numa(G1NUMA::create()), _hrm(), @@ -1613,6 +1713,11 @@ jint G1CollectedHeap::initialize() { _service_thread->register_task(_revise_young_length_task); } + if (G1UseTimeBasedHeapSizing) { + _heap_evaluation_task = new G1HeapEvaluationTask(this, _heap_sizing_policy); + _service_thread->register_task(_heap_evaluation_task); + } + // Here we allocate the dummy G1HeapRegion that is required by the // G1AllocRegion class. G1HeapRegion* dummy_region = _hrm.get_dummy_region(); @@ -2707,6 +2812,11 @@ void G1CollectedHeap::prepare_for_mutator_after_young_collection() { start_new_collection_set(); _allocator->init_mutator_alloc_regions(); + // Reset timestamps for time-based heap sizing + if (G1UseTimeBasedHeapSizing) { + _hrm.reset_free_region_timestamps(); + } + phase_times()->record_prepare_for_mutator_time_ms((Ticks::now() - start).seconds() * 1000.0); } diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp index b5cb9167d9281..b5efa32499ec1 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp @@ -34,6 +34,7 @@ #include "gc/g1/g1ConcurrentMark.hpp" #include "gc/g1/g1EdenRegions.hpp" #include "gc/g1/g1EvacStats.hpp" +#include "gc/g1/g1HeapEvaluationTask.hpp" #include "gc/g1/g1HeapRegionAttr.hpp" #include "gc/g1/g1HeapRegionManager.hpp" #include "gc/g1/g1HeapRegionSet.hpp" @@ -149,6 +150,7 @@ class G1CollectedHeap : public CollectedHeap { friend class VM_G1CollectForAllocation; friend class VM_G1CollectFull; friend class VM_G1TryInitiateConcMark; + friend class VM_G1ShrinkHeap; friend class VMStructs; friend class MutatorAllocRegion; friend class G1FullCollector; @@ -217,6 +219,8 @@ class G1CollectedHeap : public CollectedHeap { // The block offset table for the G1 heap. G1BlockOffsetTable* _bot; + G1HeapEvaluationTask* _heap_evaluation_task; + public: void rebuild_free_region_list(); // Start a new incremental collection set for the next pause. @@ -614,6 +618,10 @@ class G1CollectedHeap : public CollectedHeap { bool expand(size_t expand_bytes, WorkerThreads* pretouch_workers); bool expand_single_region(uint node_index); + // Request an immediate heap contraction of (at most) the given number of bytes. + // Uses time-based region selection to shrink oldest eligible regions. + void request_heap_shrink(size_t shrink_bytes); + // Returns the PLAB statistics for a given destination. inline G1EvacStats* alloc_buffer_stats(G1HeapRegionAttr dest); @@ -757,6 +765,10 @@ class G1CollectedHeap : public CollectedHeap { void shrink(size_t shrink_bytes); void shrink_helper(size_t expand_bytes); + // Time-based shrinking that selects oldest regions instead of from end + void shrink_with_time_based_selection(size_t shrink_bytes); + void shrink_helper_with_time_based_selection(size_t shrink_bytes); + // Schedule the VM operation that will do an evacuation pause to // satisfy an allocation request of word_size. *succeeded will // return whether the VM operation was successful (it did do an @@ -940,6 +952,8 @@ class G1CollectedHeap : public CollectedHeap { // The current policy object for the collector. G1Policy* policy() const { return _policy; } + G1HeapSizingPolicy* heap_sizing_policy() const { return _heap_sizing_policy; } + G1HeapRegionManager& heap_region_manager() { return _hrm; } // The remembered set. G1RemSet* rem_set() const { return _rem_set; } diff --git a/src/hotspot/share/gc/g1/g1HeapEvaluationTask.cpp b/src/hotspot/share/gc/g1/g1HeapEvaluationTask.cpp new file mode 100644 index 0000000000000..eaa40ce4b3954 --- /dev/null +++ b/src/hotspot/share/gc/g1/g1HeapEvaluationTask.cpp @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#include "gc/g1/g1CollectedHeap.hpp" +#include "gc/g1/g1CollectedHeap.inline.hpp" +#include "gc/g1/g1HeapEvaluationTask.hpp" +#include "gc/g1/g1HeapSizingPolicy.hpp" +#include "gc/g1/g1ServiceThread.hpp" +#include "gc/shared/suspendibleThreadSet.hpp" +#include "logging/log.hpp" +#include "memory/resourceArea.hpp" +#include "runtime/globals.hpp" +#include "utilities/debug.hpp" +#include "utilities/globalDefinitions.hpp" + +G1HeapEvaluationTask::G1HeapEvaluationTask(G1CollectedHeap* g1h, G1HeapSizingPolicy* heap_sizing_policy) : + G1ServiceTask("G1 Heap Evaluation Task"), + _g1h(g1h), + _heap_sizing_policy(heap_sizing_policy) { +} + +void G1HeapEvaluationTask::execute() { + log_debug(gc, sizing)("Starting uncommit evaluation."); + + size_t resize_amount; + + // Join suspendible thread set for proper GC synchronization + { + SuspendibleThreadSetJoiner sts; + resize_amount = _heap_sizing_policy->evaluate_heap_resize_for_uncommit(); + } + + static int evaluation_count = 0; + + if (resize_amount > 0) { + log_info(gc, sizing)("Uncommit evaluation: shrinking heap by %zuMB (%zuB) using time-based selection.", + resize_amount / M, resize_amount); + // Request VM operation outside of suspendible thread set. + _g1h->request_heap_shrink(resize_amount); + } else { + if (++evaluation_count % 10 == 0) { // Log every 10th evaluation when no action taken. + log_info(gc, sizing)("Uncommit evaluation: no heap uncommit needed (evaluation #%d)", evaluation_count); + } + } + + // Schedule the next evaluation. + schedule(G1TimeBasedEvaluationIntervalMillis); +} diff --git a/src/hotspot/share/gc/g1/g1HeapEvaluationTask.hpp b/src/hotspot/share/gc/g1/g1HeapEvaluationTask.hpp new file mode 100644 index 0000000000000..0ab6446f53f71 --- /dev/null +++ b/src/hotspot/share/gc/g1/g1HeapEvaluationTask.hpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#ifndef SHARE_GC_G1_G1HEAPEVALUATIONTASK_HPP +#define SHARE_GC_G1_G1HEAPEVALUATIONTASK_HPP + +#include "gc/g1/g1_globals.hpp" +#include "gc/g1/g1ServiceThread.hpp" + +class G1CollectedHeap; +class G1HeapSizingPolicy; + +// Time-based heap evaluation task that runs on the G1 service thread. +// Uses G1ServiceTask for better integration with G1 lifecycle and scheduling. +class G1HeapEvaluationTask : public G1ServiceTask { + G1CollectedHeap* _g1h; + G1HeapSizingPolicy* _heap_sizing_policy; + +public: + G1HeapEvaluationTask(G1CollectedHeap* g1h, G1HeapSizingPolicy* heap_sizing_policy); + virtual void execute() override; +}; + +#endif // SHARE_GC_G1_G1HEAPEVALUATIONTASK_HPP diff --git a/src/hotspot/share/gc/g1/g1HeapRegion.cpp b/src/hotspot/share/gc/g1/g1HeapRegion.cpp index 2052a3ce156a5..06c987bbc4cfb 100644 --- a/src/hotspot/share/gc/g1/g1HeapRegion.cpp +++ b/src/hotspot/share/gc/g1/g1HeapRegion.cpp @@ -156,6 +156,7 @@ void G1HeapRegion::clear_both_card_tables() { void G1HeapRegion::set_free() { if (!is_free()) { report_region_type_change(G1HeapRegionTraceType::Free); + _last_access_timestamp = Ticks::now(); // Record timestamp when region becomes free. } _type.set_free(); } @@ -260,6 +261,7 @@ G1HeapRegion::G1HeapRegion(uint hrm_index, _surv_rate_group(nullptr), _age_index(G1SurvRateGroup::InvalidAgeIndex), _node_index(G1NUMA::UnknownNodeIndex), + _last_access_timestamp(), // Default-initialized (0); set properly when region transitions to free after first use _pinned_object_count(0) { assert(Universe::on_page_boundary(mr.start()) && Universe::on_page_boundary(mr.end()), diff --git a/src/hotspot/share/gc/g1/g1HeapRegion.hpp b/src/hotspot/share/gc/g1/g1HeapRegion.hpp index ec9cab260495d..53a6e46e5fbc1 100644 --- a/src/hotspot/share/gc/g1/g1HeapRegion.hpp +++ b/src/hotspot/share/gc/g1/g1HeapRegion.hpp @@ -35,7 +35,10 @@ #include "gc/shared/verifyOption.hpp" #include "runtime/atomic.hpp" #include "runtime/mutex.hpp" +#include "runtime/os.hpp" +#include "utilities/globalDefinitions.hpp" #include "utilities/macros.hpp" +#include "utilities/ticks.hpp" class G1CardSet; class G1CardSetConfiguration; @@ -71,6 +74,8 @@ class nmethod; class G1HeapRegion : public CHeapObj { friend class VMStructs; + +private: HeapWord* const _bottom; HeapWord* const _end; @@ -249,6 +254,9 @@ class G1HeapRegion : public CHeapObj { // NUMA node. uint _node_index; + // Time-based heap sizing: tracks when region became free. + Ticks _last_access_timestamp; + // Number of objects in this region that are currently pinned. Atomic _pinned_object_count; @@ -552,6 +560,11 @@ class G1HeapRegion : public CHeapObj { uint node_index() const { return _node_index; } void set_node_index(uint node_index) { _node_index = node_index; } + // Time-based heap sizing methods. + Ticks last_access_time() const { + return _last_access_timestamp; + } + // Verify that the entries on the code root list for this // region are live and include at least one pointer into this region. // Returns whether there has been a failure. diff --git a/src/hotspot/share/gc/g1/g1HeapRegionManager.cpp b/src/hotspot/share/gc/g1/g1HeapRegionManager.cpp index 3c0318827efcc..bf75e8d8d8787 100644 --- a/src/hotspot/share/gc/g1/g1HeapRegionManager.cpp +++ b/src/hotspot/share/gc/g1/g1HeapRegionManager.cpp @@ -68,7 +68,8 @@ G1HeapRegionManager::G1HeapRegionManager() : _next_highest_used_hrm_index(0), _regions(), _heap_mapper(nullptr), _bitmap_mapper(nullptr), - _free_list("Free list", new G1MasterFreeRegionListChecker()) + _free_list("Free list", new G1MasterFreeRegionListChecker()), + _last_gc_timestamp() { } void G1HeapRegionManager::initialize(G1RegionToSpaceMapper* heap_storage, @@ -583,6 +584,10 @@ void G1HeapRegionManager::par_iterate(G1HeapRegionClosure* blk, G1HeapRegionClai } uint G1HeapRegionManager::shrink_by(uint num_regions_to_remove) { + return shrink_by(num_regions_to_remove, false); +} + +uint G1HeapRegionManager::shrink_by(uint num_regions_to_remove, bool use_time_based_selection) { assert(num_committed_regions() > 0, "the region sequence should not be empty"); assert(num_committed_regions() <= _next_highest_used_hrm_index, "invariant"); assert(_next_highest_used_hrm_index > 0, "we should have at least one region committed"); @@ -593,18 +598,25 @@ uint G1HeapRegionManager::shrink_by(uint num_regions_to_remove) { } uint removed = 0; - uint cur = _next_highest_used_hrm_index; - uint idx_last_found = 0; - uint num_last_found = 0; - while ((removed < num_regions_to_remove) && - (num_last_found = find_empty_from_idx_reverse(cur, &idx_last_found)) > 0) { - uint to_remove = MIN2(num_regions_to_remove - removed, num_last_found); + if (use_time_based_selection) { + // Time-based selection: find oldest empty regions across the entire heap + removed = shrink_by_time_based_selection(num_regions_to_remove); + } else { + // Traditional selection: find empty regions from the end of heap (existing behavior) + uint cur = _next_highest_used_hrm_index; + uint idx_last_found = 0; + uint num_last_found = 0; - shrink_at(idx_last_found + num_last_found - to_remove, to_remove); + while ((removed < num_regions_to_remove) && + (num_last_found = find_empty_from_idx_reverse(cur, &idx_last_found)) > 0) { + uint to_remove = MIN2(num_regions_to_remove - removed, num_last_found); - cur = idx_last_found; - removed += to_remove; + shrink_at(idx_last_found + num_last_found - to_remove, to_remove); + + cur = idx_last_found; + removed += to_remove; + } } verify_optional(); @@ -612,6 +624,87 @@ uint G1HeapRegionManager::shrink_by(uint num_regions_to_remove) { return removed; } +void G1HeapRegionManager::reset_free_region_timestamps() { + _last_gc_timestamp = Ticks::now(); + log_trace(gc, heap)("Updated GC timestamp baseline for time-based heap sizing"); +} + +uint G1HeapRegionManager::shrink_by_time_based_selection(uint num_regions_to_remove) { + // Collect all empty regions with their access times for sorting + GrowableArray empty_regions; + + // Scan all committed regions to find free ones. + Ticks current_time = Ticks::now(); + Ticks last_gc_time = _last_gc_timestamp; + + class CollectIdleRegionsClosure : public G1HeapRegionClosure { + GrowableArray* _empty_regions; + Ticks _current_time; + Ticks _last_gc_time; + public: + CollectIdleRegionsClosure(GrowableArray* empty_regions, + Ticks current_time, + Ticks last_gc_time) : + _empty_regions(empty_regions), + _current_time(current_time), + _last_gc_time(last_gc_time) {} + + virtual bool do_heap_region(G1HeapRegion* r) { + if (r->is_free()) { + // Effective time is later of region timestamp or last GC time + Ticks region_time = r->last_access_time(); + Ticks effective_time = MAX2(region_time, _last_gc_time); + Tickspan elapsed = _current_time - effective_time; + if (elapsed.milliseconds() > G1UncommitDelayMillis) { + _empty_regions->append(r); + } + } + return false; + } + } cl(&empty_regions, current_time, last_gc_time); + + iterate(&cl); + + if (empty_regions.length() == 0) { + log_debug(gc, sizing)("Time-based shrink: no eligible empty regions found"); + return 0; + } + + // Sort by access time (oldest first) + static auto compare_region_age = [](G1HeapRegion** a, G1HeapRegion** b) -> int { + Ticks time_a = (*a)->last_access_time(); + Ticks time_b = (*b)->last_access_time(); + if (time_a < time_b) return -1; + if (time_a > time_b) return 1; + return 0; + }; + empty_regions.sort(compare_region_age); + + // Shrink the oldest regions first + uint removed = 0; + uint regions_to_process = MIN2(num_regions_to_remove, (uint)empty_regions.length()); + + log_debug(gc, sizing)("Time-based shrink: processing %u oldest regions out of %d empty regions", + regions_to_process, empty_regions.length()); + + for (uint i = 0; i < regions_to_process; i++) { + G1HeapRegion* hr = empty_regions.at(i); + uint region_index = hr->hrm_index(); + + log_debug(gc, sizing)("Time-based shrink: deactivating region %u (last_access=" UINT64_FORMAT "ms ago)", + region_index, (Ticks::now() - hr->last_access_time()).milliseconds()); + + shrink_at(region_index, 1); + removed++; + } + + if (removed > 0) { + log_info(gc, sizing)("Time-based shrink: deactivated %u oldest empty regions", removed); + } + + return removed; +} + void G1HeapRegionManager::shrink_at(uint index, size_t num_regions) { #ifdef ASSERT for (uint i = index; i < (index + num_regions); i++) { diff --git a/src/hotspot/share/gc/g1/g1HeapRegionManager.hpp b/src/hotspot/share/gc/g1/g1HeapRegionManager.hpp index eb593ff408e6b..cde99312f1b1a 100644 --- a/src/hotspot/share/gc/g1/g1HeapRegionManager.hpp +++ b/src/hotspot/share/gc/g1/g1HeapRegionManager.hpp @@ -32,6 +32,7 @@ #include "memory/allocation.hpp" #include "runtime/atomic.hpp" #include "services/memoryUsage.hpp" +#include "utilities/ticks.hpp" class G1HeapRegion; class G1HeapRegionClaimer; @@ -126,6 +127,9 @@ class G1HeapRegionManager: public CHeapObj { G1RegionToSpaceMapper* _bitmap_mapper; G1FreeRegionList _free_list; + // Baseline timestamp for time-based heap sizing (updated after each GC) + Ticks _last_gc_timestamp; + void expand(uint index, uint num_regions, WorkerThreads* pretouch_workers = nullptr); // G1RegionCommittedMap helpers. These functions do the work that comes with @@ -138,6 +142,9 @@ class G1HeapRegionManager: public CHeapObj { void reactivate_regions(uint start, uint num_regions); void uncommit_regions(uint start, uint num_regions); + // Time-based shrinking helper: find and shrink oldest empty regions + uint shrink_by_time_based_selection(uint num_regions_to_remove); + // Allocate a new G1HeapRegion for the given index. G1HeapRegion* new_heap_region(uint hrm_index); @@ -272,6 +279,7 @@ class G1HeapRegionManager: public CHeapObj { // Uncommit up to num_regions_to_remove regions that are completely free. // Return the actual number of uncommitted regions. uint shrink_by(uint num_regions_to_remove); + uint shrink_by(uint num_regions_to_remove, bool use_time_based_selection); // Remove a number of regions starting at the specified index, which must be available, // empty, and free. The regions are marked inactive and can later be uncommitted. @@ -284,6 +292,9 @@ class G1HeapRegionManager: public CHeapObj { // actual number uncommitted. uint uncommit_inactive_regions(uint limit); + // Record baseline timestamp for time-based heap sizing (O(1)) + void reset_free_region_timestamps(); + void verify(); // Do some sanity checking. diff --git a/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp b/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp index 1b9704e8ad323..c347e93b276e4 100644 --- a/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp +++ b/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp @@ -23,13 +23,23 @@ */ #include "gc/g1/g1Analytics.hpp" +#include "gc/g1/g1CollectedHeap.hpp" #include "gc/g1/g1CollectedHeap.inline.hpp" +#include "gc/g1/g1HeapRegion.hpp" +#include "gc/g1/g1HeapRegionManager.inline.hpp" #include "gc/g1/g1HeapSizingPolicy.hpp" +#include "gc/g1/g1Policy.hpp" +#include "gc/g1/g1_globals.hpp" // For flag declarations #include "gc/shared/gc_globals.hpp" #include "logging/log.hpp" +#include "memory/resourceArea.hpp" #include "runtime/globals.hpp" +#include "runtime/mutexLocker.hpp" +#include "runtime/os.hpp" +#include "runtime/safepoint.hpp" #include "utilities/debug.hpp" #include "utilities/globalDefinitions.hpp" +#include "utilities/ticks.hpp" G1HeapSizingPolicy* G1HeapSizingPolicy::create(const G1CollectedHeap* g1h, const G1Analytics* analytics) { return new G1HeapSizingPolicy(g1h, analytics); @@ -43,6 +53,7 @@ G1HeapSizingPolicy::G1HeapSizingPolicy(const G1CollectedHeap* g1h, const G1Analy _gc_cpu_usage_deviation_counter((G1CPUUsageExpandThreshold / 2) + 1), _recent_cpu_usage_deltas(long_term_count_limit()), _long_term_count(0) { + assert(_analytics != nullptr, "analytics must not be null"); } void G1HeapSizingPolicy::reset_cpu_usage_tracking_data() { @@ -438,3 +449,218 @@ size_t G1HeapSizingPolicy::full_collection_resize_amount(bool& expand, size_t al expand = true; // Does not matter. return 0; } + +uint G1HeapSizingPolicy::count_uncommit_candidates() { + uint idle_regions = 0; + + // Count regions that would be eligible for uncommit. + class CountUncommitCandidatesClosure : public G1HeapRegionClosure { + uint& _idle_regions; + const G1HeapSizingPolicy* _policy; + public: + CountUncommitCandidatesClosure(uint& idle_regions, const G1HeapSizingPolicy* policy) : + _idle_regions(idle_regions), + _policy(policy) {} + + virtual bool do_heap_region(G1HeapRegion* r) { + // Note: All free regions are empty, so only check is_free() + if (r->is_free() && _policy->should_uncommit_region(r)) { + _idle_regions++; + } + return false; + } + } cl(idle_regions, this); + + log_debug(gc, sizing)("Full region scan: counting uncommit candidates"); + _g1h->heap_region_iterate(&cl); + return idle_regions; +} + +void G1HeapSizingPolicy::find_uncommit_candidates_by_time(GrowableArray* candidates) { + uint idle_regions = 0; + + // Check each heap region for inactivity, limiting to candidates capacity. + class UncommitCandidatesClosure : public G1HeapRegionClosure { + GrowableArray* _candidates; + uint& _idle_regions; + const G1HeapSizingPolicy* _policy; + public: + UncommitCandidatesClosure(GrowableArray* candidates, + uint& idle_regions, + const G1HeapSizingPolicy* policy) : + _candidates(candidates), + _idle_regions(idle_regions), + _policy(policy) {} + + virtual bool do_heap_region(G1HeapRegion* r) { + // Note: All free regions are empty, so only check is_free(). + if (r->is_free() && _policy->should_uncommit_region(r)) { + _candidates->append(r); + _idle_regions++; + // Stop early if we have enough candidates. + if (_candidates->length() >= _candidates->capacity()) { + return true; // Stop iteration. + } + } + return false; + } + } cl(candidates, idle_regions, this); + + _g1h->heap_region_iterate(&cl); + + if (idle_regions > 0) { + log_debug(gc, sizing)("Time-based uncommit evaluation: found %u idle regions (max %d)", + idle_regions, candidates->capacity()); + } +} + +size_t G1HeapSizingPolicy::calculate_time_based_shrink_amount(uint max_regions_to_shrink) { + ResourceMark rm; + + GrowableArray candidates(max_regions_to_shrink); + + // Find time-based candidates. + find_uncommit_candidates_by_time(&candidates); + + if (candidates.length() == 0) { + log_debug(gc, sizing)("Time-based shrink: no candidates found"); + return 0; + } + + uint valid_candidates = (uint)candidates.length(); + size_t shrink_bytes = (size_t)valid_candidates * G1HeapRegion::GrainBytes; + + if (valid_candidates > 0) { + log_info(gc, sizing)("Time-based shrink: requesting %zuMB based on %u time-based candidates", + shrink_bytes / M, valid_candidates); + } + + return shrink_bytes; +} + +bool G1HeapSizingPolicy::should_uncommit_region(G1HeapRegion* hr) const { + Ticks current_time = Ticks::now(); + Ticks last_access = hr->last_access_time(); + Tickspan elapsed = current_time - last_access; + + log_trace(gc, sizing)("Region %u uncommit check: elapsed=" JLONG_FORMAT "ms threshold=" JLONG_FORMAT "ms last_access=" JLONG_FORMAT " now=" JLONG_FORMAT " empty=%s", + hr->hrm_index(), (jlong)elapsed.milliseconds(), (jlong)G1UncommitDelayMillis, last_access.value(), current_time.value(), + hr->is_empty() ? "true" : "false"); + + bool should_uncommit = elapsed.milliseconds() > G1UncommitDelayMillis; + if (should_uncommit) { + log_debug(gc, sizing)("Region %u transitioning to idle after " JLONG_FORMAT "ms.", + hr->hrm_index(), (jlong)elapsed.milliseconds()); + } + + return should_uncommit; +} + +size_t G1HeapSizingPolicy::evaluate_heap_resize_for_uncommit() { + if (!G1UseTimeBasedHeapSizing) { + return 0; + } + + // Skip uncommit if GC overhead exceeds threshold (125% of GCTimeRatio goal) + double gc_time_ratio = _analytics->short_term_gc_time_ratio(); + double gc_time_goal = 1.0 / (1.0 + GCTimeRatio); + double gc_time_threshold = gc_time_goal * 1.25; + + if (gc_time_ratio > gc_time_threshold) { + log_trace(gc, sizing)("Uncommit evaluation: skipping, GC overhead (%1.1f%%) exceeds " + "threshold (%1.1f%% of %1.1f%% goal)", + gc_time_ratio * 100.0, + gc_time_threshold * 100.0, + gc_time_goal * 100.0); + return 0; + } + + MutexLocker ml(Heap_lock); + + // Count regions eligible for uncommit (don't store them - VM operation will re-evaluate). + uint idle_count = count_uncommit_candidates(); + + log_debug(gc, sizing)("Uncommit evaluation: found %u idle candidates (min required: %zu)", + idle_count, (size_t)G1MinRegionsToUncommit); + + // Need minimum number of idle regions to proceed. + if (idle_count >= G1MinRegionsToUncommit) { + size_t region_size = G1HeapRegion::GrainBytes; + size_t current_capacity = _g1h->capacity(); + size_t min_heap = MAX2(InitialHeapSize, MinHeapSize); // Never go below initial size. + + // Max bytes we can uncommit while respecting min heap size + size_t max_shrink_bytes = current_capacity > min_heap ? current_capacity - min_heap : 0; + + log_trace(gc, sizing)("Uncommit evaluation: current_capacity=%zuB min_heap=%zuB " + "region_size=%zuB max_shrink=%zuB initial_size=%zuB", + current_capacity, min_heap, region_size, max_shrink_bytes, InitialHeapSize); + + if (max_shrink_bytes > 0) { + size_t committed_regions = current_capacity / region_size; + + // G1ReservePercent reserves free space for allocation bursts. + size_t g1_reserve_regions = (size_t)ceil((double)committed_regions * G1ReservePercent / 100.0); + // Young gen regions are committed and in use. + size_t young_gen_regions = _g1h->policy()->young_list_target_length(); + + // Minimum committed = young gen (in use) + reserve buffer (free). + size_t min_committed_regions = g1_reserve_regions + young_gen_regions; + + log_debug(gc, sizing)("Uncommit evaluation: regions analysis - committed=%zu, idle=%u, " + "young_gen=%zu, g1_reserve=%zu, min_committed=%zu", + committed_regions, idle_count, young_gen_regions, g1_reserve_regions, + min_committed_regions); + + if (committed_regions <= min_committed_regions) { + log_debug(gc, sizing)("Time-based uncommit: no excess regions beyond minimum " + "(committed=%zu <= min_committed=%zu)", + committed_regions, min_committed_regions); + log_info(gc, sizing)("Uncommit evaluation: no heap uncommit needed (no excess regions)"); + return 0; + } + + size_t available_for_uncommit = idle_count; + + size_t max_idle_regions = max_shrink_bytes / region_size; + + // Limit uncommit to a small fraction of committed regions + size_t max_uncommit_at_once = MAX2((size_t)G1MinRegionsToUncommit, committed_regions / 8); + size_t regions_to_uncommit = MIN3(available_for_uncommit, max_idle_regions, max_uncommit_at_once); + + size_t shrink_bytes = regions_to_uncommit * region_size; + shrink_bytes = MIN2(shrink_bytes, current_capacity - MinHeapSize); + + if (current_capacity - shrink_bytes < InitialHeapSize) { + log_info(gc, sizing)("Uncommit evaluation: skipped, would reduce heap below initial size (%zuMB < %zuMB)", + (current_capacity - shrink_bytes) / M, InitialHeapSize / M); + log_debug(gc, sizing)("Skipping uncommit - would reduce heap below initial size: " + "current=%zuB shrink=%zuB result=%zuB initial=%zuB min=%zuB", + current_capacity, shrink_bytes, current_capacity - shrink_bytes, + InitialHeapSize, MinHeapSize); + return 0; + } + + if (shrink_bytes > 0) { + log_info(gc, sizing)("Uncommit evaluation: found %u idle regions, uncommitting %zu regions (%zuMB).", + idle_count, regions_to_uncommit, shrink_bytes / M); + log_debug(gc, sizing)("Uncommit evaluation: target shrink %zuB (max allowed %zuB).", + shrink_bytes, max_shrink_bytes); + + // Calculate shrink amount based on time-based candidates + size_t time_based_shrink = calculate_time_based_shrink_amount((uint)regions_to_uncommit); + + return time_based_shrink; + } + + return 0; + } + } + + log_info(gc, sizing)("Uncommit evaluation: no heap uncommit needed " + "(idle=%u min_required=%zu heap=%zuB min=%zuB)", + idle_count, (size_t)G1MinRegionsToUncommit, + _g1h->capacity(), MAX2(InitialHeapSize, MinHeapSize)); + + return 0; +} diff --git a/src/hotspot/share/gc/g1/g1HeapSizingPolicy.hpp b/src/hotspot/share/gc/g1/g1HeapSizingPolicy.hpp index 32096f692b428..721bb1f36526f 100644 --- a/src/hotspot/share/gc/g1/g1HeapSizingPolicy.hpp +++ b/src/hotspot/share/gc/g1/g1HeapSizingPolicy.hpp @@ -25,9 +25,15 @@ #ifndef SHARE_GC_G1_G1HEAPSIZINGPOLICY_HPP #define SHARE_GC_G1_G1HEAPSIZINGPOLICY_HPP +#include "gc/g1/g1_globals.hpp" #include "gc/g1/g1Analytics.hpp" +#include "gc/g1/g1HeapRegion.hpp" #include "memory/allocation.hpp" +#include "runtime/globals.hpp" +#include "utilities/debug.hpp" +#include "utilities/globalDefinitions.hpp" #include "utilities/numberSeq.hpp" +#include "utilities/ticks.hpp" class G1CollectedHeap; @@ -93,8 +99,8 @@ class G1HeapSizingPolicy: public CHeapObj { size_t young_collection_shrink_amount(double cpu_usage_delta, size_t allocation_word_size) const; G1HeapSizingPolicy(const G1CollectedHeap* g1h, const G1Analytics* analytics); -public: +public: static constexpr uint long_term_count_limit() { return G1Analytics::max_num_of_recorded_pause_times(); } @@ -107,6 +113,17 @@ class G1HeapSizingPolicy: public CHeapObj { // should by expanded by that amount, shrunk otherwise. size_t full_collection_resize_amount(bool& expand, size_t allocation_word_size); + // Time-based sizing methods + size_t evaluate_heap_resize_for_uncommit(); + + // Methods for time-based sizing analysis + uint count_uncommit_candidates(); + void find_uncommit_candidates_by_time(GrowableArray* candidates); + bool should_uncommit_region(G1HeapRegion* hr) const; + + // Mark specific time-based candidates as idle for uncommitting + size_t calculate_time_based_shrink_amount(uint max_regions_to_shrink); + static G1HeapSizingPolicy* create(const G1CollectedHeap* g1h, const G1Analytics* analytics); }; diff --git a/src/hotspot/share/gc/g1/g1InitLogger.cpp b/src/hotspot/share/gc/g1/g1InitLogger.cpp index 1838b55e4d3ed..1c0963c5fd063 100644 --- a/src/hotspot/share/gc/g1/g1InitLogger.cpp +++ b/src/hotspot/share/gc/g1/g1InitLogger.cpp @@ -48,6 +48,13 @@ void G1InitLogger::print_gc_specific() { } else { log_info_p(gc, init)("Periodic GC: Disabled"); } + + // Print a message about time-based heap sizing configuration. + if (G1UseTimeBasedHeapSizing) { + log_info_p(gc, init)("G1 Time-Based Heap Sizing enabled (uncommit-only)"); + log_info_p(gc, init)(" Evaluation Interval: %zus, Uncommit Delay: %zus, Min Regions To Uncommit: %zu", + G1TimeBasedEvaluationIntervalMillis / 1000, G1UncommitDelayMillis / 1000, G1MinRegionsToUncommit); + } } void G1InitLogger::print() { diff --git a/src/hotspot/share/gc/g1/g1RegionToSpaceMapper.cpp b/src/hotspot/share/gc/g1/g1RegionToSpaceMapper.cpp index 5e37c7fa5a11f..df341cc296016 100644 --- a/src/hotspot/share/gc/g1/g1RegionToSpaceMapper.cpp +++ b/src/hotspot/share/gc/g1/g1RegionToSpaceMapper.cpp @@ -267,6 +267,7 @@ class G1RegionsSmallerThanCommitSizeMapper : public G1RegionToSpaceMapper { _storage.uncommit(uncommitted_l, num_uncommitted_pages_found); } } + }; void G1RegionToSpaceMapper::fire_on_commit(uint start_idx, size_t num_regions, bool zero_filled) { diff --git a/src/hotspot/share/gc/g1/g1VMOperations.cpp b/src/hotspot/share/gc/g1/g1VMOperations.cpp index f98f0b078f33c..ffaeab18a5df7 100644 --- a/src/hotspot/share/gc/g1/g1VMOperations.cpp +++ b/src/hotspot/share/gc/g1/g1VMOperations.cpp @@ -22,8 +22,10 @@ * */ +#include "gc/g1/g1Allocator.hpp" #include "gc/g1/g1CollectedHeap.inline.hpp" #include "gc/g1/g1ConcurrentMarkThread.inline.hpp" +#include "gc/g1/g1HeapSizingPolicy.hpp" #include "gc/g1/g1Policy.hpp" #include "gc/g1/g1Trace.hpp" #include "gc/g1/g1VMOperations.hpp" @@ -175,3 +177,51 @@ void VM_G1PauseCleanup::work() { G1ConcurrentMark* cm = G1CollectedHeap::heap()->concurrent_mark(); cm->cleanup(); } + +bool VM_G1ShrinkHeap::skip_operation() const { + // A GC occurred since we scheduled this operation; skip shrinking since + // GC already determined the appropriate heap size. + if (_g1h->total_collections() != _gc_count_before) { + log_debug(gc, ergo, heap)("VM_G1ShrinkHeap: skipping - GC occurred since scheduling"); + return true; + } + return VM_GC_Operation::skip_operation(); +} + +void VM_G1ShrinkHeap::doit() { + // Re-evaluate candidates at safepoint since heap state may have changed. + log_debug(gc, ergo, heap)("VM_G1ShrinkHeap: re-evaluating heap state at safepoint"); + + // Max regions based on original request + uint max_regions_to_shrink = (uint)(_bytes / G1HeapRegion::GrainBytes); + + GrowableArray candidates(max_regions_to_shrink); + _g1h->heap_sizing_policy()->find_uncommit_candidates_by_time(&candidates); + + if (candidates.length() == 0) { + log_debug(gc, ergo, heap)("VM_G1ShrinkHeap: no valid candidates at safepoint, skipping shrink"); + return; + } + + // Validate candidates are still free at safepoint + uint valid_count = 0; + for (int i = 0; i < candidates.length(); i++) { + G1HeapRegion* hr = candidates.at(i); + if (hr->is_free()) { + valid_count++; + } else { + log_debug(gc, ergo, heap)("VM_G1ShrinkHeap: skipping region %u - no longer free", hr->hrm_index()); + } + } + + if (valid_count == 0) { + log_debug(gc, ergo, heap)("VM_G1ShrinkHeap: no regions still valid at safepoint"); + return; + } + + size_t shrink_bytes = (size_t)valid_count * G1HeapRegion::GrainBytes; + log_info(gc, ergo, heap)("VM_G1ShrinkHeap: executing shrink with %u regions (%zuMB) after re-evaluation", + valid_count, shrink_bytes / M); + + _g1h->shrink_with_time_based_selection(shrink_bytes); +} diff --git a/src/hotspot/share/gc/g1/g1VMOperations.hpp b/src/hotspot/share/gc/g1/g1VMOperations.hpp index 5429051496ac3..396706028e4bc 100644 --- a/src/hotspot/share/gc/g1/g1VMOperations.hpp +++ b/src/hotspot/share/gc/g1/g1VMOperations.hpp @@ -30,6 +30,8 @@ // VM_operations for the G1 collector. +class G1CollectedHeap; + class VM_G1CollectFull : public VM_GC_Collect_Operation { protected: bool skip_operation() const override; @@ -108,4 +110,20 @@ class VM_G1PauseCleanup : public VM_G1PauseConcurrent { void work() override; }; +class VM_G1ShrinkHeap : public VM_GC_Operation { + private: + G1CollectedHeap* _g1h; + size_t _bytes; // Maximum bytes to shrink (used as hint for re-evaluation). + + protected: + bool skip_operation() const override; + + public: + VM_G1ShrinkHeap(G1CollectedHeap* g1h, uint gc_count_before, size_t bytes) + : VM_GC_Operation(gc_count_before, GCCause::_g1_periodic_collection, 0, false), + _g1h(g1h), _bytes(bytes) {} + VMOp_Type type() const override { return VMOp_G1ShrinkHeap; } + void doit() override; // Re-evaluates regions at safepoint. +}; + #endif // SHARE_GC_G1_G1VMOPERATIONS_HPP diff --git a/src/hotspot/share/gc/g1/g1_globals.hpp b/src/hotspot/share/gc/g1/g1_globals.hpp index b338c11d5be20..f54e5137d240d 100644 --- a/src/hotspot/share/gc/g1/g1_globals.hpp +++ b/src/hotspot/share/gc/g1/g1_globals.hpp @@ -371,6 +371,25 @@ "scan cost related prediction samples. A sample must involve " \ "the same or more than this number of code roots to be used.") \ \ + product(bool, G1UseTimeBasedHeapSizing, true, DIAGNOSTIC, \ + "Enable time-based heap sizing to uncommit memory from idle " \ + "regions independent of GC cycles") \ + \ + product(uintx, G1TimeBasedEvaluationIntervalMillis, 60000, MANAGEABLE, \ + "Interval in milliseconds between periodic heap-size evaluations "\ + "when G1UseTimeBasedHeapSizing is enabled") \ + range(1000, LP64_ONLY(max_jlong) NOT_LP64(max_uintx / 2)) \ + \ + product(uintx, G1UncommitDelayMillis, 300000, MANAGEABLE, \ + "A region is considered idle if it has not been accessed " \ + "within this many milliseconds") \ + range(1000, LP64_ONLY(max_jlong) NOT_LP64(max_uintx / 2)) \ + \ + product(size_t, G1MinRegionsToUncommit, 10, DIAGNOSTIC, \ + "Minimum number of idle regions required before G1 will " \ + "attempt to uncommit memory") \ + range(1, max_uintx) \ + \ develop(bool, G1ForceOptionalEvacuation, false, \ "Force optional evacuation for all GCs where there are old gen " \ "collection set candidates." \ diff --git a/src/hotspot/share/logging/logTag.hpp b/src/hotspot/share/logging/logTag.hpp index 20d61b542b08d..5959e3d4d1ec9 100644 --- a/src/hotspot/share/logging/logTag.hpp +++ b/src/hotspot/share/logging/logTag.hpp @@ -181,6 +181,7 @@ class outputStream; LOG_TAG(scavenge) \ LOG_TAG(sealed) \ LOG_TAG(setting) \ + LOG_TAG(sizing) \ LOG_TAG(smr) \ LOG_TAG(stackbarrier) \ LOG_TAG(stackmap) \ diff --git a/src/hotspot/share/runtime/vmOperation.hpp b/src/hotspot/share/runtime/vmOperation.hpp index 5140d0401fb92..35db36cd76cbd 100644 --- a/src/hotspot/share/runtime/vmOperation.hpp +++ b/src/hotspot/share/runtime/vmOperation.hpp @@ -57,6 +57,7 @@ template(G1CollectFull) \ template(G1PauseRemark) \ template(G1PauseCleanup) \ + template(G1ShrinkHeap) \ template(G1TryInitiateConcMark) \ template(G1RendezvousGCThreads) \ template(ZMarkEndOld) \ diff --git a/test/hotspot/jtreg/gc/g1/TestG1RegionUncommit.java b/test/hotspot/jtreg/gc/g1/TestG1RegionUncommit.java new file mode 100644 index 0000000000000..6a354b0c5c70f --- /dev/null +++ b/test/hotspot/jtreg/gc/g1/TestG1RegionUncommit.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package gc.g1; + +/** + * @test TestG1RegionUncommit + * @requires vm.gc.G1 + * @summary Test that G1 uncommits regions based on time threshold + * @bug 8357445 + * @library /test/lib + * @modules java.base/jdk.internal.misc + * java.management/sun.management + * @run main/othervm -XX:+UseG1GC -Xms8m -Xmx256m -XX:G1HeapRegionSize=1M + * -XX:+UnlockDiagnosticVMOptions + * -XX:G1UncommitDelayMillis=3000 -XX:G1TimeBasedEvaluationIntervalMillis=2000 + * -XX:G1MinRegionsToUncommit=2 + * -Xlog:gc*,gc+sizing*=debug + * gc.g1.TestG1RegionUncommit + */ + +import java.util.ArrayList; +import java.util.List; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; + +public class TestG1RegionUncommit { + + public static void main(String[] args) throws Exception { + // If no args, run the subprocess with log analysis + if (args.length == 0) { + testTimeBasedEvaluation(); + testMinimumHeapBoundary(); + testConcurrentAllocationUncommit(); + } else if ("subprocess".equals(args[0])) { + // This is the subprocess that does the actual allocation/deallocation + runTimeBasedUncommitTest(); + } else if ("minheap".equals(args[0])) { + runMinHeapBoundaryTest(); + } else if ("concurrent".equals(args[0])) { + runConcurrentTest(); + } + } + + static void testTimeBasedEvaluation() throws Exception { + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder( + "-XX:+UseG1GC", + "-Xms8m", "-Xmx256m", "-XX:G1HeapRegionSize=1M", + "-XX:+UnlockDiagnosticVMOptions", + "-XX:G1UncommitDelayMillis=3000", "-XX:G1TimeBasedEvaluationIntervalMillis=2000", + "-XX:G1MinRegionsToUncommit=2", + "-Xlog:gc*,gc+sizing*=debug", + "gc.g1.TestG1RegionUncommit", "subprocess" + ); + + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + // Verify the uncommit evaluation logic is working + output.shouldContain("G1 Time-Based Heap Sizing enabled (uncommit-only)"); + output.shouldContain("Starting uncommit evaluation"); + + // The test should show either successful uncommit or reasons why uncommit didn't happen + // Both are valid outcomes showing the evaluation system is working + if (output.getStdout().contains("Region state transition:") || + output.getStdout().contains("Uncommit evaluation: Found") || + output.getStdout().contains("Uncommit candidates found") || + output.getStdout().contains("no heap uncommit needed")) { + // Test passed - evaluation system is working + System.out.println("Time-based evaluation system is working"); + } else { + // If none of the expected evaluation messages appear, that's a failure + output.shouldContain("Uncommit evaluation:"); + } + + output.shouldHaveExitValue(0); + System.out.println("Test passed - time-based uncommit verified!"); + } + + static void runTimeBasedUncommitTest() throws Exception { + final int allocSize = 64 * 1024 * 1024; // 64MB allocation - much larger than initial 8MB + Object keepAlive; + Object keepAlive2; // Keep some memory allocated to prevent full shrinkage + + System.out.println("=== Testing G1 Time-Based Uncommit ==="); + + // Phase 1: Allocate memory to force significant heap expansion + System.out.println("Phase 1: Allocating large amount of memory"); + keepAlive = new byte[allocSize]; + + // Phase 2: Keep some memory allocated, free the rest to create inactive regions + // This ensures current_heap > min_heap so uncommit is possible + System.out.println("Phase 2: Partially freeing memory, keeping some allocated"); + keepAlive2 = new byte[24 * 1024 * 1024]; // Keep 24MB allocated + keepAlive = null; // Free the 64MB, leaving regions available for uncommit + System.gc(); + System.gc(); // Double GC to ensure the 64MB is cleaned up + + // Phase 3: Wait for regions to become inactive and uncommit to occur + System.out.println("Phase 3: Waiting for time-based uncommit..."); + + // Wait long enough for: + // 1. G1UncommitDelayMillis (3000ms) - regions to become inactive + // 2. G1TimeBasedEvaluationIntervalMillis (2000ms) - evaluation to run + // 3. Multiple evaluation cycles to ensure uncommit happens + Thread.sleep(15000); // 15 seconds should be plenty + + // Clean up remaining allocation + keepAlive2 = null; + System.gc(); + + System.out.println("=== Test completed ==="); + Runtime.getRuntime().halt(0); + } + + static void testMinimumHeapBoundary() throws Exception { + System.out.println("Testing minimum heap boundary conditions..."); + + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder( + "-XX:+UseG1GC", + "-Xms32m", "-Xmx64m", // Small heap to test boundaries + "-XX:G1HeapRegionSize=1M", + "-XX:+UnlockDiagnosticVMOptions", + "-XX:G1UncommitDelayMillis=2000", // Short delay + "-XX:G1TimeBasedEvaluationIntervalMillis=1000", + "-XX:G1MinRegionsToUncommit=1", + "-Xlog:gc+sizing=debug,gc+task=debug", + "gc.g1.TestG1RegionUncommit", "minheap" + ); + + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + // Should not uncommit below initial heap size + output.shouldHaveExitValue(0); + System.out.println("Minimum heap boundary test passed!"); + } + + static void testConcurrentAllocationUncommit() throws Exception { + System.out.println("Testing concurrent allocation and uncommit..."); + + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder( + "-XX:+UseG1GC", + "-Xms64m", "-Xmx256m", + "-XX:G1HeapRegionSize=1M", + "-XX:+UnlockDiagnosticVMOptions", + "-XX:G1TimeBasedEvaluationIntervalMillis=1000", // Frequent evaluation + "-XX:G1UncommitDelayMillis=2000", + "-XX:G1MinRegionsToUncommit=2", + "-Xlog:gc+sizing=debug,gc+task=debug", + "gc.g1.TestG1RegionUncommit", "concurrent" + ); + + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + // Should handle concurrent operations safely + output.shouldHaveExitValue(0); + System.out.println("Concurrent allocation/uncommit test passed!"); + } + + static void runMinHeapBoundaryTest() throws Exception { + System.out.println("=== Min Heap Boundary Test ==="); + + List memory = new ArrayList<>(); + + // Allocate close to max + for (int i = 0; i < 28; i++) { // 28MB, close to 32MB limit + memory.add(new byte[1024 * 1024]); + } + + // Clear and wait for uncommit attempt + memory.clear(); + System.gc(); + Thread.sleep(8000); // Wait longer than uncommit delay + + System.out.println("MinHeapBoundaryTest completed"); + Runtime.getRuntime().halt(0); + } + + static void runConcurrentTest() throws Exception { + System.out.println("=== Concurrent Test ==="); + + final List sharedMemory = new ArrayList<>(); + final boolean[] stopFlag = {false}; + + // Start allocation thread + Thread allocThread = new Thread(() -> { + int iterations = 0; + while (!stopFlag[0] && iterations < 50) { + try { + // Allocate + for (int j = 0; j < 5; j++) { + synchronized (sharedMemory) { + sharedMemory.add(new byte[1024 * 1024]); // 1MB + } + Thread.sleep(10); + } + + // Clear some + synchronized (sharedMemory) { + if (sharedMemory.size() > 10) { + for (int k = 0; k < 5; k++) { + if (!sharedMemory.isEmpty()) { + sharedMemory.remove(0); + } + } + } + } + System.gc(); + Thread.sleep(50); + iterations++; + } catch (InterruptedException e) { + break; + } + } + }); + + allocThread.start(); + + // Let it run for a while to trigger time-based evaluation + Thread.sleep(8000); + + stopFlag[0] = true; + allocThread.join(2000); + + synchronized (sharedMemory) { + sharedMemory.clear(); + } + System.gc(); + + System.out.println("ConcurrentTest completed"); + Runtime.getRuntime().halt(0); + } +} diff --git a/test/hotspot/jtreg/gc/g1/TestTimeBasedHeapConfig.java b/test/hotspot/jtreg/gc/g1/TestTimeBasedHeapConfig.java new file mode 100644 index 0000000000000..af25f5f20ca73 --- /dev/null +++ b/test/hotspot/jtreg/gc/g1/TestTimeBasedHeapConfig.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package gc.g1; + +/** + * @test TestTimeBasedHeapConfig + * @bug 8357445 + * @summary Test configuration settings and error conditions for time-based heap sizing + * @requires vm.gc.G1 + * @library /test/lib + * @modules java.base/jdk.internal.misc + * java.management/sun.management + * @run main/othervm -XX:+UseG1GC -XX:+UnlockDiagnosticVMOptions + * -Xms16m -Xmx64m -XX:G1HeapRegionSize=1M + * -XX:G1TimeBasedEvaluationIntervalMillis=5000 + * -XX:G1UncommitDelayMillis=10000 + * -XX:G1MinRegionsToUncommit=2 + * -Xlog:gc*,gc+sizing*=debug + * gc.g1.TestTimeBasedHeapConfig + */ + +import java.util.*; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; + +public class TestTimeBasedHeapConfig { + + public static void main(String[] args) throws Exception { + testConfigurationParameters(); + testBoundaryValues(); + testEdgeCaseConfigurations(); + } + + static void testConfigurationParameters() throws Exception { + // Test default settings + verifyVMConfig(new String[] { + "-XX:+UseG1GC", + "-XX:+UnlockDiagnosticVMOptions", + "-Xms16m", "-Xmx64m", + "-XX:G1HeapRegionSize=1M", + "-Xlog:gc*,gc+sizing*=debug", + "gc.g1.TestTimeBasedHeapConfig$BasicTest" + }); + } + + private static void verifyVMConfig(String[] opts) throws Exception { + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(opts); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + output.shouldHaveExitValue(0); + } + + public static class BasicTest { + private static final int MB = 1024 * 1024; + private static ArrayList arrays = new ArrayList<>(); + + public static void main(String[] args) throws Exception { + // Initial allocation + allocateMemory(8); // 8MB + System.gc(); + Thread.sleep(1000); + + // Clean up + arrays.clear(); + System.gc(); + Thread.sleep(2000); + + System.out.println("Basic configuration test completed successfully"); + Runtime.getRuntime().halt(0); + } + + static void allocateMemory(int mb) throws InterruptedException { + for (int i = 0; i < mb; i++) { + arrays.add(new byte[MB]); + if (i % 2 == 0) Thread.sleep(10); + } + } + } + + static void testBoundaryValues() throws Exception { + // Test minimum values + verifyVMConfig(new String[] { + "-XX:+UseG1GC", + "-XX:+UnlockDiagnosticVMOptions", + "-Xms8m", "-Xmx32m", + "-XX:G1HeapRegionSize=1M", + "-XX:G1TimeBasedEvaluationIntervalMillis=1000", // 1 second minimum + "-XX:G1UncommitDelayMillis=1000", // 1 second minimum + "-XX:G1MinRegionsToUncommit=1", // 1 region minimum + "-Xlog:gc*,gc+sizing*=debug", + "gc.g1.TestTimeBasedHeapConfig$BoundaryTest" + }); + + // Test maximum reasonable values + verifyVMConfig(new String[] { + "-XX:+UseG1GC", + "-XX:+UnlockDiagnosticVMOptions", + "-Xms32m", "-Xmx256m", + "-XX:G1HeapRegionSize=1M", + "-XX:G1TimeBasedEvaluationIntervalMillis=300000", // 5 minutes + "-XX:G1UncommitDelayMillis=300000", // 5 minutes + "-XX:G1MinRegionsToUncommit=50", // 50 regions + "-Xlog:gc*,gc+sizing*=debug", + "gc.g1.TestTimeBasedHeapConfig$BoundaryTest" + }); + } + + static void testEdgeCaseConfigurations() throws Exception { + // Test with very small heap (should still work) + verifyVMConfig(new String[] { + "-XX:+UseG1GC", + "-XX:+UnlockDiagnosticVMOptions", + "-Xms4m", "-Xmx8m", // Very small heap + "-XX:G1HeapRegionSize=1M", + "-XX:G1TimeBasedEvaluationIntervalMillis=2000", + "-XX:G1UncommitDelayMillis=3000", + "-XX:G1MinRegionsToUncommit=1", + "-Xlog:gc*,gc+sizing*=debug", + "gc.g1.TestTimeBasedHeapConfig$SmallHeapTest" + }); + } + + public static class BoundaryTest { + private static final int MB = 1024 * 1024; + private static ArrayList arrays = new ArrayList<>(); + + public static void main(String[] args) throws Exception { + System.out.println("BoundaryTest: Starting"); + + // Test with boundary conditions + allocateMemory(4); // 4MB + Thread.sleep(2000); + + arrays.clear(); + System.gc(); + Thread.sleep(5000); // Wait for evaluation + + System.out.println("BoundaryTest: Completed"); + Runtime.getRuntime().halt(0); + } + + static void allocateMemory(int mb) throws InterruptedException { + for (int i = 0; i < mb; i++) { + arrays.add(new byte[MB]); + Thread.sleep(10); + } + } + } + + public static class SmallHeapTest { + public static void main(String[] args) throws Exception { + System.out.println("SmallHeapTest: Starting with very small heap"); + + // With 4-8MB heap, just allocate a small amount + byte[] smallAlloc = new byte[1024 * 1024]; // 1MB + Thread.sleep(2000); + + smallAlloc = null; + System.gc(); + Thread.sleep(5000); + + System.out.println("SmallHeapTest: Completed"); + Runtime.getRuntime().halt(0); + } + } +} diff --git a/test/hotspot/jtreg/gc/g1/TestTimeBasedHeapSizing.java b/test/hotspot/jtreg/gc/g1/TestTimeBasedHeapSizing.java new file mode 100644 index 0000000000000..636de0c286ca4 --- /dev/null +++ b/test/hotspot/jtreg/gc/g1/TestTimeBasedHeapSizing.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package gc.g1; + +/** + * @test TestTimeBasedHeapSizing + * @bug 8357445 + * @summary Test time-based heap sizing functionality in G1 + * @requires vm.gc.G1 + * @library /test/lib + * @modules java.base/jdk.internal.misc + * java.management/sun.management + * @run main/othervm -XX:+UseG1GC -XX:+UnlockDiagnosticVMOptions + * -Xms32m -Xmx128m -XX:G1HeapRegionSize=1M + * -XX:G1TimeBasedEvaluationIntervalMillis=5000 + * -XX:G1UncommitDelayMillis=10000 + * -XX:G1MinRegionsToUncommit=2 + * -Xlog:gc*,gc+sizing*=debug + * gc.g1.TestTimeBasedHeapSizing + */ + +import java.util.*; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; + +public class TestTimeBasedHeapSizing { + + private static final String TEST_VM_OPTS = "-XX:+UseG1GC " + + "-XX:+UnlockDiagnosticVMOptions " + + "-XX:G1TimeBasedEvaluationIntervalMillis=5000 " + + "-XX:G1UncommitDelayMillis=10000 " + + "-XX:G1MinRegionsToUncommit=2 " + + "-XX:G1HeapRegionSize=1M " + + "-Xmx128m -Xms32m " + + "-Xlog:gc*,gc+sizing*=debug"; + + public static void main(String[] args) throws Exception { + testBasicFunctionality(); + testHumongousObjectHandling(); + testRapidAllocationCycles(); + testLargeHumongousObjects(); + } + + static void testBasicFunctionality() throws Exception { + String[] command = new String[TEST_VM_OPTS.split(" ").length + 1]; + System.arraycopy(TEST_VM_OPTS.split(" "), 0, command, 0, TEST_VM_OPTS.split(" ").length); + command[command.length - 1] = "gc.g1.TestTimeBasedHeapSizing$BasicFunctionalityTest"; + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(command); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + output.shouldContain("G1 Time-Based Heap Sizing enabled (uncommit-only)"); + output.shouldContain("Starting uncommit evaluation"); + output.shouldContain("Full region scan:"); + + output.shouldHaveExitValue(0); + } + + public static class BasicFunctionalityTest { + private static final int MB = 1024 * 1024; + private static ArrayList arrays = new ArrayList<>(); + + public static void main(String[] args) throws Exception { + System.out.println("BasicFunctionalityTest: Starting heap activity"); + + // Create significant heap activity + for (int cycle = 0; cycle < 3; cycle++) { + System.out.println("Allocation cycle " + cycle); + allocateMemory(25); // 25MB per cycle + Thread.sleep(200); // Brief pause + clearMemory(); + System.gc(); + Thread.sleep(200); + } + + System.out.println("BasicFunctionalityTest: Starting idle period"); + + // Sleep to allow time-based evaluation + Thread.sleep(18000); // 18 seconds + + System.out.println("BasicFunctionalityTest: Completed idle period"); + + // Final cleanup + clearMemory(); + Thread.sleep(500); + + System.out.println("BasicFunctionalityTest: Test completed"); + Runtime.getRuntime().halt(0); + } + + static void allocateMemory(int mb) throws InterruptedException { + for (int i = 0; i < mb; i++) { + arrays.add(new byte[MB]); + if (i % 4 == 0) Thread.sleep(10); + } + } + + static void clearMemory() { + arrays.clear(); + System.gc(); + } + } + + static void testHumongousObjectHandling() throws Exception { + String[] command = new String[TEST_VM_OPTS.split(" ").length + 1]; + System.arraycopy(TEST_VM_OPTS.split(" "), 0, command, 0, TEST_VM_OPTS.split(" ").length); + command[command.length - 1] = "gc.g1.TestTimeBasedHeapSizing$HumongousObjectTest"; + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(command); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + output.shouldContain("Starting uncommit evaluation"); + output.shouldHaveExitValue(0); + } + + static void testRapidAllocationCycles() throws Exception { + String[] command = new String[TEST_VM_OPTS.split(" ").length + 1]; + System.arraycopy(TEST_VM_OPTS.split(" "), 0, command, 0, TEST_VM_OPTS.split(" ").length); + command[command.length - 1] = "gc.g1.TestTimeBasedHeapSizing$RapidCycleTest"; + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(command); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + output.shouldContain("Starting uncommit evaluation"); + output.shouldHaveExitValue(0); + } + + static void testLargeHumongousObjects() throws Exception { + System.out.println("Testing large humongous object activity tracking..."); + + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder( + "-XX:+UseG1GC", + "-XX:+UnlockDiagnosticVMOptions", + "-Xms64m", "-Xmx256m", + "-XX:G1HeapRegionSize=1M", + "-XX:G1UncommitDelayMillis=5000", + "-XX:G1MinRegionsToUncommit=1", + "-Xlog:gc*,gc+sizing*=debug", + "gc.g1.TestTimeBasedHeapSizing$LargeHumongousTest" + ); + + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + // Large humongous objects should not affect uncommit safety + output.shouldContain("G1 Time-Based Heap Sizing enabled (uncommit-only)"); + output.shouldHaveExitValue(0); + System.out.println("Large humongous object test passed!"); + } + + public static class HumongousObjectTest { + private static final int MB = 1024 * 1024; + private static ArrayList humongousObjects = new ArrayList<>(); + + public static void main(String[] args) throws Exception { + System.out.println("HumongousObjectTest: Starting"); + + // Allocate humongous objects (> 512KB for 1MB regions) + for (int i = 0; i < 8; i++) { + humongousObjects.add(new byte[800 * 1024]); // 800KB humongous + System.out.println("Allocated humongous object " + (i + 1)); + Thread.sleep(200); + } + + // Keep them alive for a while + Thread.sleep(3000); + + // Clear and test uncommit behavior + humongousObjects.clear(); + System.gc(); + Thread.sleep(12000); // Wait for uncommit delay + + System.out.println("HumongousObjectTest: Test completed"); + Runtime.getRuntime().halt(0); + } + } + + public static class RapidCycleTest { + private static final int MB = 1024 * 1024; + private static ArrayList memory = new ArrayList<>(); + + public static void main(String[] args) throws Exception { + System.out.println("RapidCycleTest: Starting"); + + // Rapid allocation/deallocation cycles + for (int cycle = 0; cycle < 15; cycle++) { + // Quick allocation + for (int i = 0; i < 8; i++) { + memory.add(new byte[MB]); // 1MB + } + + // Quick deallocation + memory.clear(); + System.gc(); + + // Brief pause + Thread.sleep(100); + + if (cycle % 5 == 0) { + System.out.println("Completed cycle " + cycle); + } + } + + // Final wait for time-based evaluation + Thread.sleep(12000); + + System.out.println("RapidCycleTest: Test completed"); + Runtime.getRuntime().halt(0); + } + } + + public static class LargeHumongousTest { + public static void main(String[] args) throws Exception { + System.out.println("=== Large Humongous Object Test ==="); + + // Allocate several large humongous objects (multiple regions each) + List humongousObjects = new ArrayList<>(); + + // Each region is 1MB, so allocate 2MB objects (humongous spanning multiple regions) + for (int i = 0; i < 5; i++) { + humongousObjects.add(new byte[2 * 1024 * 1024]); + System.gc(); // Force potential region transitions + Thread.sleep(100); + } + + // Hold some, release others to create mixed region states + humongousObjects.remove(0); + humongousObjects.remove(0); + System.gc(); + + // Wait for time-based evaluation with humongous regions present + Thread.sleep(8000); + + // Clean up + humongousObjects.clear(); + System.gc(); + + System.out.println("LargeHumongousTest: Test completed"); + Runtime.getRuntime().halt(0); + } + } +} diff --git a/test/hotspot/jtreg/gc/g1/TestTimeBasedRegionTracking.java b/test/hotspot/jtreg/gc/g1/TestTimeBasedRegionTracking.java new file mode 100644 index 0000000000000..e1837465cb381 --- /dev/null +++ b/test/hotspot/jtreg/gc/g1/TestTimeBasedRegionTracking.java @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package gc.g1; + +/** + * @test TestTimeBasedRegionTracking + * @bug 8357445 + * @summary Test region activity tracking and state transitions for time-based heap sizing + * @requires vm.gc.G1 + * @library /test/lib + * @modules java.base/jdk.internal.misc + * java.management/sun.management + * @run main/othervm/timeout=120 -XX:+UseG1GC -XX:+UnlockDiagnosticVMOptions + * -Xms32m -Xmx128m -XX:G1HeapRegionSize=1M + * -XX:G1TimeBasedEvaluationIntervalMillis=5000 + * -XX:G1UncommitDelayMillis=10000 + * -XX:G1MinRegionsToUncommit=2 + * -Xlog:gc*,gc+sizing*=debug gc.g1.TestTimeBasedRegionTracking + */ + +import java.util.*; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; +import java.util.concurrent.atomic.AtomicBoolean; + +public class TestTimeBasedRegionTracking { + + private static final String TEST_VM_OPTS = "-XX:+UseG1GC " + + "-XX:+UnlockDiagnosticVMOptions " + + "-XX:G1TimeBasedEvaluationIntervalMillis=5000 " + + "-XX:G1UncommitDelayMillis=10000 " + + "-XX:G1MinRegionsToUncommit=2 " + + "-XX:G1HeapRegionSize=1M " + + "-Xmx128m -Xms32m " + + "-Xlog:gc*,gc+sizing*=debug"; + + public static void main(String[] args) throws Exception { + testRegionStateTransitions(); + testConcurrentRegionAccess(); + testRegionLifecycleEdgeCases(); + testSafepointRaceConditions(); + } + + static void testRegionStateTransitions() throws Exception { + String[] command = new String[TEST_VM_OPTS.split(" ").length + 1]; + System.arraycopy(TEST_VM_OPTS.split(" "), 0, command, 0, TEST_VM_OPTS.split(" ").length); + command[command.length - 1] = "gc.g1.TestTimeBasedRegionTracking$RegionTransitionTest"; + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(command); + + Process process = pb.start(); + OutputAnalyzer output = new OutputAnalyzer(process); + + // Verify region state changes and basic functionality + // Check for key log messages that indicate the feature is working + if (output.getStdout().contains("Region state transition:") || + output.getStdout().contains("Uncommit candidates found:") || + output.getStdout().contains("Starting uncommit evaluation")) { + System.out.println("Time-based evaluation system is working"); + } else { + // If none of the expected messages appear, that's a failure + output.shouldContain("Starting uncommit evaluation"); + } + + output.shouldHaveExitValue(0); + } + + public static class RegionTransitionTest { + private static final int MB = 1024 * 1024; + private static ArrayList arrays = new ArrayList<>(); + + public static void main(String[] args) throws Exception { + System.out.println("RegionTransitionTest: Starting"); + + // Phase 1: Active allocation + allocateMemory(20); // Reduced from 32MB for faster execution + System.gc(); + + // Phase 2: Idle period + arrays.clear(); + System.gc(); + Thread.sleep(12000); // Reduced wait time - should still trigger uncommit + + // Phase 3: Reallocation + allocateMemory(10); // Smaller reallocation + System.gc(); + + // Clean up and wait for final uncommit evaluation + arrays = null; + System.gc(); + Thread.sleep(1000); // Shorter final wait + + System.out.println("RegionTransitionTest: Test completed"); + Runtime.getRuntime().halt(0); + } + + static void allocateMemory(int mb) throws InterruptedException { + for (int i = 0; i < mb; i++) { + arrays.add(new byte[MB]); + if (i % 4 == 0) Thread.sleep(10); + } + } + } + + static void testConcurrentRegionAccess() throws Exception { + String[] command = new String[TEST_VM_OPTS.split(" ").length + 1]; + System.arraycopy(TEST_VM_OPTS.split(" "), 0, command, 0, TEST_VM_OPTS.split(" ").length); + command[command.length - 1] = "gc.g1.TestTimeBasedRegionTracking$ConcurrentAccessTest"; + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(command); + + Process process = pb.start(); + OutputAnalyzer output = new OutputAnalyzer(process); + + // Verify concurrent access is handled safely + output.shouldHaveExitValue(0); + } + + static void testRegionLifecycleEdgeCases() throws Exception { + String[] command = new String[TEST_VM_OPTS.split(" ").length + 1]; + System.arraycopy(TEST_VM_OPTS.split(" "), 0, command, 0, TEST_VM_OPTS.split(" ").length); + command[command.length - 1] = "gc.g1.TestTimeBasedRegionTracking$RegionLifecycleEdgeCaseTest"; + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(command); + + Process process = pb.start(); + OutputAnalyzer output = new OutputAnalyzer(process); + + // Verify region lifecycle edge cases are handled + output.shouldHaveExitValue(0); + } + + static void testSafepointRaceConditions() throws Exception { + System.out.println("Testing safepoint and allocation race conditions..."); + + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder( + "-XX:+UseG1GC", + "-XX:+UnlockDiagnosticVMOptions", + "-Xms64m", "-Xmx256m", + "-XX:G1HeapRegionSize=1M", + "-XX:G1TimeBasedEvaluationIntervalMillis=5000", // More reasonable interval + "-XX:G1UncommitDelayMillis=3000", // Shorter delay for faster test + "-XX:G1MinRegionsToUncommit=1", + "-Xlog:gc*,gc+sizing*=debug", + "gc.g1.TestTimeBasedRegionTracking$SafepointRaceTest" + ); + + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + // Should handle safepoint races without errors + output.shouldContain("G1 Time-Based Heap Sizing enabled (uncommit-only)"); + output.shouldHaveExitValue(0); + System.out.println("Safepoint race conditions test passed!"); + } + + public static class ConcurrentAccessTest { + private static final int MB = 1024 * 1024; + private static final List sharedMemory = new ArrayList<>(); + private static volatile boolean stopThreads = false; + + public static void main(String[] args) throws Exception { + System.out.println("ConcurrentAccessTest: Starting"); + + // Start multiple allocation threads + Thread[] threads = new Thread[3]; + for (int t = 0; t < threads.length; t++) { + final int threadId = t; + threads[t] = new Thread(() -> { + int iterations = 0; + while (!stopThreads && iterations < 30) { + try { + // Allocate + for (int i = 0; i < 3; i++) { + synchronized (sharedMemory) { + sharedMemory.add(new byte[512 * 1024]); // 512KB + } + Thread.sleep(10); + } + + // Clear some memory + synchronized (sharedMemory) { + if (sharedMemory.size() > 15) { + for (int i = 0; i < 5; i++) { + if (!sharedMemory.isEmpty()) { + sharedMemory.remove(0); + } + } + } + } + + if (iterations % 10 == 0) { + System.gc(); + } + + iterations++; + Thread.sleep(50); + } catch (InterruptedException e) { + break; + } + } + System.out.println("Thread " + threadId + " completed " + iterations + " iterations"); + }); + threads[t].start(); + } + + // Let threads run for a shorter time + Thread.sleep(5000); // Reduced from 8000ms + + stopThreads = true; + for (Thread t : threads) { + t.join(1000); // Reduced join timeout + } + + synchronized (sharedMemory) { + sharedMemory.clear(); + } + System.gc(); + Thread.sleep(1000); // Reduced from 3000ms + + System.out.println("ConcurrentAccessTest: Test completed"); + Runtime.getRuntime().halt(0); + } + } + + public static class RegionLifecycleEdgeCaseTest { + private static final int MB = 1024 * 1024; + private static List memory = new ArrayList<>(); + + public static void main(String[] args) throws Exception { + System.out.println("RegionLifecycleEdgeCaseTest: Starting"); + + // Phase 1: Mixed allocation patterns + // Small objects + for (int i = 0; i < 100; i++) { + memory.add(new byte[8 * 1024]); // 8KB objects + } + + // Medium objects + for (int i = 0; i < 20; i++) { + memory.add(new byte[40 * 1024]); // 40KB objects + } + + // Large objects (but not humongous) + for (int i = 0; i < 5; i++) { + memory.add(new byte[300 * 1024]); // 300KB objects + } + + Thread.sleep(2000); + + // Phase 2: Create fragmentation by selective deallocation + for (int i = memory.size() - 1; i >= 0; i -= 2) { + memory.remove(i); + } + + System.gc(); + Thread.sleep(3000); + + // Phase 3: Add humongous objects + for (int i = 0; i < 3; i++) { + memory.add(new byte[900 * 1024]); // 900KB humongous + Thread.sleep(500); + } + + Thread.sleep(2000); + + // Phase 4: Final cleanup + memory.clear(); + System.gc(); + Thread.sleep(6000); // Reduced wait time but still allow for evaluation + + System.out.println("RegionLifecycleEdgeCaseTest: Test completed"); + Runtime.getRuntime().halt(0); + } + } + + public static class SafepointRaceTest { + public static void main(String[] args) throws Exception { + System.out.println("=== Safepoint Race Conditions Test ==="); + + final AtomicBoolean stopFlag = new AtomicBoolean(false); + final List sharedMemory = Collections.synchronizedList(new ArrayList<>()); + + // Start fewer threads with reduced iterations for faster completion + Thread[] threads = new Thread[2]; + for (int i = 0; i < threads.length; i++) { + final int threadId = i; + threads[i] = new Thread(() -> { + int iteration = 0; + while (!stopFlag.get() && iteration < 10) { // Reduced iterations + try { + // Allocate and deallocate + for (int j = 0; j < 3; j++) { // Fewer allocations per iteration + sharedMemory.add(new byte[256 * 1024]); // Smaller allocations + } + + // Force GC less frequently + if (iteration % 5 == 0) { + System.gc(); + } + + // Clear some allocations + synchronized (sharedMemory) { + if (sharedMemory.size() > 6) { + for (int k = 0; k < 2; k++) { + if (!sharedMemory.isEmpty()) { + sharedMemory.remove(0); + } + } + } + } + + Thread.sleep(50); // Shorter pause + iteration++; + } catch (InterruptedException e) { + break; + } + } + System.out.println("Thread " + threadId + " completed"); + }); + threads[i].start(); + } + + // Much shorter run time - just enough for one evaluation cycle + Thread.sleep(4000); + + // Stop threads + stopFlag.set(true); + for (Thread thread : threads) { + thread.join(1000); + } + + // Clean up + sharedMemory.clear(); + System.gc(); + + // Wait for one more evaluation cycle to see some activity + Thread.sleep(2000); + + System.out.println("SafepointRaceTest: Test completed"); + Runtime.getRuntime().halt(0); + } + } +}