From 494c1c1744230eff774a02bf88e6500eef3c3e4c Mon Sep 17 00:00:00 2001 From: Monica Beckwith Date: Wed, 28 May 2025 17:13:04 -0500 Subject: [PATCH] 8357445: Implement time-based heap size re-evaluation for G1GC --- src/hotspot/share/gc/g1/g1Allocator.cpp | 8 + src/hotspot/share/gc/g1/g1Allocator.hpp | 7 + src/hotspot/share/gc/g1/g1CollectedHeap.cpp | 27 +- src/hotspot/share/gc/g1/g1CollectedHeap.hpp | 2 + .../share/gc/g1/g1HeapEvaluationTask.cpp | 63 +++ .../share/gc/g1/g1HeapEvaluationTask.hpp | 42 ++ src/hotspot/share/gc/g1/g1HeapRegion.cpp | 2 + src/hotspot/share/gc/g1/g1HeapRegion.hpp | 3 + .../share/gc/g1/g1HeapSizingPolicy.cpp | 65 ++++ .../share/gc/g1/g1HeapSizingPolicy.hpp | 17 +- src/hotspot/share/gc/g1/g1_globals.hpp | 26 +- .../jtreg/gc/g1/TestG1RegionUncommit.java | 79 ++++ .../jtreg/gc/g1/TestTimeBasedHeapConfig.java | 187 +++++++++ .../jtreg/gc/g1/TestTimeBasedHeapSizing.java | 362 ++++++++++++++++++ .../gc/g1/TestTimeBasedRegionTracking.java | 225 +++++++++++ 15 files changed, 1109 insertions(+), 6 deletions(-) create mode 100644 src/hotspot/share/gc/g1/g1HeapEvaluationTask.cpp create mode 100644 src/hotspot/share/gc/g1/g1HeapEvaluationTask.hpp create mode 100644 test/hotspot/jtreg/gc/g1/TestG1RegionUncommit.java create mode 100644 test/hotspot/jtreg/gc/g1/TestTimeBasedHeapConfig.java create mode 100644 test/hotspot/jtreg/gc/g1/TestTimeBasedHeapSizing.java create mode 100644 test/hotspot/jtreg/gc/g1/TestTimeBasedRegionTracking.java diff --git a/src/hotspot/share/gc/g1/g1Allocator.cpp b/src/hotspot/share/gc/g1/g1Allocator.cpp index c44234fa11c..f39fcce32f4 100644 --- a/src/hotspot/share/gc/g1/g1Allocator.cpp +++ b/src/hotspot/share/gc/g1/g1Allocator.cpp @@ -248,6 +248,14 @@ HeapWord* G1Allocator::survivor_attempt_allocation(uint node_index, HeapWord* result = survivor_gc_alloc_region(node_index)->attempt_allocation(min_word_size, desired_word_size, actual_word_size); + // Record activity on successful allocation + if (result != nullptr) { + G1HeapRegion* hr = _g1h->heap_region_containing(result); + if (hr != nullptr) { + hr->record_activity(); + } + } + if (result == nullptr && !survivor_is_full()) { MutexLocker x(FreeList_lock, Mutex::_no_safepoint_check_flag); // Multiple threads may have queued at the FreeList_lock above after checking whether there diff --git a/src/hotspot/share/gc/g1/g1Allocator.hpp b/src/hotspot/share/gc/g1/g1Allocator.hpp index 8c382824b27..e652cc64d46 100644 --- a/src/hotspot/share/gc/g1/g1Allocator.hpp +++ b/src/hotspot/share/gc/g1/g1Allocator.hpp @@ -32,6 +32,7 @@ class G1EvacInfo; class G1NUMA; +class G1HeapSizingPolicy; // Interface to keep track of which regions G1 is currently allocating into. Provides // some accessors (e.g. allocating into them, or getting their occupancy). @@ -72,6 +73,12 @@ class G1Allocator : public CHeapObj { OldGCAllocRegion* old, G1HeapRegion** retained); + void record_region_activity(G1HeapRegion* hr) { + if (hr != nullptr) { + hr->record_activity(); + } + } + // Accessors to the allocation regions. inline MutatorAllocRegion* mutator_alloc_region(uint node_index); inline SurvivorGCAllocRegion* survivor_gc_alloc_region(uint node_index); diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp index 6df9383ce22..dc124948164 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp @@ -1193,6 +1193,8 @@ G1CollectedHeap::G1CollectedHeap() : _is_subject_to_discovery_cm(this), _region_attr() { + _heap_evaluation_task = nullptr; + _verifier = new G1HeapVerifier(this); _allocator = new G1Allocator(this); @@ -1443,8 +1445,12 @@ jint G1CollectedHeap::initialize() { _free_arena_memory_task = new G1MonotonicArenaFreeMemoryTask("Card Set Free Memory Task"); _service_thread->register_task(_free_arena_memory_task); - // Here we allocate the dummy G1HeapRegion that is required by the - // G1AllocRegion class. + 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(); // We'll re-use the same region whether the alloc region will @@ -2889,6 +2895,7 @@ void G1CollectedHeap::retire_mutator_alloc_region(G1HeapRegion* alloc_region, assert_heap_locked_or_at_safepoint(true /* should_be_vm_thread */); assert(alloc_region->is_eden(), "all mutator alloc regions should be eden"); + alloc_region->record_activity(); // Update region access time collection_set()->add_eden_region(alloc_region); increase_used(allocated_bytes); _eden.add_used_bytes(allocated_bytes); @@ -3083,3 +3090,19 @@ void G1CollectedHeap::prepare_group_cardsets_for_scan() { collection_set()->prepare_groups_for_scan(); } + +bool G1CollectedHeap::check_region_for_uncommit(HeapRegion* hr) { + // First check if region is empty + if (!hr->is_empty()) { + log_trace(gc, heap)("Region %u not eligible for uncommit - not empty", hr->hrm_index()); + return false; + } + + bool should_uncommit = hr->should_uncommit(G1UncommitDelay); + + log_trace(gc, heap)("Region %u uncommit check: empty=%d should_uncommit=%d", + hr->hrm_index(), hr->is_empty(), should_uncommit); + + return should_uncommit; +} + diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp index f9c9525c675..5bfc4b5534d 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp @@ -192,6 +192,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. diff --git a/src/hotspot/share/gc/g1/g1HeapEvaluationTask.cpp b/src/hotspot/share/gc/g1/g1HeapEvaluationTask.cpp new file mode 100644 index 00000000000..61de2c435af --- /dev/null +++ b/src/hotspot/share/gc/g1/g1HeapEvaluationTask.cpp @@ -0,0 +1,63 @@ +/* + * 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/g1HeapEvaluationTask.hpp" +#include "gc/g1/g1CollectedHeap.hpp" +#include "gc/g1/g1HeapSizingPolicy.hpp" +#include "gc/shared/gc_globals.hpp" +#include "logging/log.hpp" +#include "runtime/thread.hpp" + +G1HeapEvaluationTask::G1HeapEvaluationTask(G1CollectedHeap* g1h, G1HeapSizingPolicy* heap_sizing_policy) : + G1ServiceTask("G1 Heap Evaluation"), + _g1h(g1h), + _heap_sizing_policy(heap_sizing_policy) { +} + +void G1HeapEvaluationTask::execute() { + if (!G1UseTimeBasedHeapSizing) { + return; + } + + log_debug(gc, sizing)("G1 periodic heap evaluation - evaluating time-based sizing"); + + // Evaluate resize based on heap sizing policy + bool expand; + size_t resize_bytes = _heap_sizing_policy->evaluate_heap_resize(expand); + + if (resize_bytes > 0) { + if (expand) { + _g1h->expand(resize_bytes, _g1h->workers()); + log_debug(gc, sizing)("Time-based expansion: " SIZE_FORMAT "B", resize_bytes); + } else { + // Shrink based on inactive regions + _g1h->shrink(resize_bytes); + log_debug(gc, sizing)("Time-based shrink: " SIZE_FORMAT "B", resize_bytes); + } + } + + // Schedule next evaluation + assert(Thread::current()->is_service_thread(), "must be service thread"); + 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 00000000000..d440227cbd0 --- /dev/null +++ b/src/hotspot/share/gc/g1/g1HeapEvaluationTask.hpp @@ -0,0 +1,42 @@ +/* + * 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/g1ServiceThread.hpp" + +class G1CollectedHeap; +class G1HeapSizingPolicy; + +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 03610ab520b..8612a0bc45e 100644 --- a/src/hotspot/share/gc/g1/g1HeapRegion.cpp +++ b/src/hotspot/share/gc/g1/g1HeapRegion.cpp @@ -119,6 +119,7 @@ void G1HeapRegion::unlink_from_list() { void G1HeapRegion::hr_clear(bool clear_space) { set_top(bottom()); + record_activity(); // Record region initialization clear_young_index_in_cset(); clear_index_in_opt_cset(); uninstall_surv_rate_group(); @@ -249,6 +250,7 @@ G1HeapRegion::G1HeapRegion(uint hrm_index, _surv_rate_group(nullptr), _age_index(G1SurvRateGroup::InvalidAgeIndex), _node_index(G1NUMA::UnknownNodeIndex), + _last_access_timestamp(os::javaTimeNanos() / NANOSECS_PER_MILLISEC), // Initialize timestamp for time-based sizing _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 1962d3173b7..c787cb5a8f4 100644 --- a/src/hotspot/share/gc/g1/g1HeapRegion.hpp +++ b/src/hotspot/share/gc/g1/g1HeapRegion.hpp @@ -560,6 +560,9 @@ class G1HeapRegion : public CHeapObj { void print_on(outputStream* st) const; bool verify(VerifyOption vo) const; + + // Get region index for heap region manager + uint region_idx() const { return hrm_index(); } }; // G1HeapRegionClosure is used for iterating over regions. diff --git a/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp b/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp index 71e25177166..da282adade2 100644 --- a/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp +++ b/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp @@ -30,6 +30,7 @@ #include "runtime/globals.hpp" #include "utilities/debug.hpp" #include "utilities/globalDefinitions.hpp" +#include "gc/g1/g1HeapRegionManager.inline.hpp" G1HeapSizingPolicy* G1HeapSizingPolicy::create(const G1CollectedHeap* g1h, const G1Analytics* analytics) { return new G1HeapSizingPolicy(g1h, analytics); @@ -291,3 +292,67 @@ size_t G1HeapSizingPolicy::full_collection_resize_amount(bool& expand, size_t al expand = true; // Does not matter. return 0; } + +size_t G1HeapSizingPolicy::_uncommit_delay_ms = 0; + +void G1HeapSizingPolicy::get_uncommit_candidates(GrowableArray* candidates) { + // Check each heap region for inactivity + for (HeapRegion* hr : _g1h->heap_region_iterate()) { + if (hr->is_empty() && should_uncommit_region(hr)) { + candidates->append(hr); + } + } +} + +bool G1HeapSizingPolicy::should_uncommit_region(G1HeapRegion* hr) const { + if (!hr->is_empty()) { + return false; + } + + jlong current_time = os::javaTimeMillis(); + jlong last_access = hr->last_access_time(); + jlong elapsed = current_time - last_access; + + log_trace(gc, heap)("Region %u uncommit check: elapsed=%lldms threshold=%lldms last_access=%lld now=%lld", + hr->hrm_index(), elapsed, (jlong)_uncommit_delay_ms, last_access, current_time); + + return elapsed > _uncommit_delay_ms; +} + +size_t G1HeapSizingPolicy::evaluate_heap_resize(bool& expand) { + expand = false; + + if (!G1UseTimeBasedHeapSizing) { + return 0; + } + + // Don't resize during GC + if (_g1h->is_gc_in_progress_mask()) { + return 0; + } + + // Find regions eligible for uncommit + GrowableArray candidates; + get_uncommit_candidates(&candidates); + + uint inactive_count = candidates.length(); + uint total_regions = _g1h->num_regions(); + + // Need minimum number of inactive regions to proceed + if (inactive_count >= MinRegionsToUncommit) { + // Target uncommitting half of inactive regions + size_t region_size = G1HeapRegion::GrainBytes; + size_t shrink_bytes = (size_t)(inactive_count * region_size * 0.5); + + log_debug(gc, sizing)("Time-based heap shrink evaluation: " + "inactive_regions=%u total_regions=%u " + "uncommit_delay=%lu ms shrink_bytes=" SIZE_FORMAT, + inactive_count, total_regions, + (ulong)_uncommit_delay_ms, shrink_bytes); + + return shrink_bytes; + } + + return 0; +} + diff --git a/src/hotspot/share/gc/g1/g1HeapSizingPolicy.hpp b/src/hotspot/share/gc/g1/g1HeapSizingPolicy.hpp index 4ee302403ed..014910dd4de 100644 --- a/src/hotspot/share/gc/g1/g1HeapSizingPolicy.hpp +++ b/src/hotspot/share/gc/g1/g1HeapSizingPolicy.hpp @@ -35,6 +35,7 @@ class G1HeapSizingPolicy: public CHeapObj { // pause times in G1Analytics, representing the minimum number of pause // time ratios that exceed GCTimeRatio before a heap expansion will be triggered. const static uint MinOverThresholdForGrowth = 4; + static size_t _uncommit_delay_ms; // Delay before uncommitting inactive regions const G1CollectedHeap* _g1h; const G1Analytics* _analytics; @@ -50,8 +51,12 @@ class G1HeapSizingPolicy: public CHeapObj { double scale_with_heap(double pause_time_threshold); G1HeapSizingPolicy(const G1CollectedHeap* g1h, const G1Analytics* analytics); -public: + // Methods for time-based sizing + void get_uncommit_candidates(GrowableArray* candidates); + bool should_uncommit_region(G1HeapRegion* hr) const; + +public: // If an expansion would be appropriate, because recent GC overhead had // exceeded the desired limit, return an amount to expand by. size_t young_collection_expansion_amount(); @@ -59,9 +64,19 @@ class G1HeapSizingPolicy: public CHeapObj { // Returns the amount of bytes to resize the heap; if expand is set, the heap // should by expanded by that amount, shrunk otherwise. size_t full_collection_resize_amount(bool& expand, size_t allocation_word_size); + // Clear ratio tracking data used by expansion_amount(). void clear_ratio_check_data(); + // Time-based sizing methods + static void initialize() { + _uncommit_delay_ms = G1UncommitDelay; + } + static size_t uncommit_delay() { + return _uncommit_delay_ms; + } + size_t evaluate_heap_resize(bool& expand); + static G1HeapSizingPolicy* create(const G1CollectedHeap* g1h, const G1Analytics* analytics); }; diff --git a/src/hotspot/share/gc/g1/g1_globals.hpp b/src/hotspot/share/gc/g1/g1_globals.hpp index 44d0d22257e..9ca0d9bb83b 100644 --- a/src/hotspot/share/gc/g1/g1_globals.hpp +++ b/src/hotspot/share/gc/g1/g1_globals.hpp @@ -348,8 +348,28 @@ product, \ product_pd, \ range, \ - constraint) - -// end of GC_G1_FLAGS + constraint) \ + \ + // Parameters controlling time-based heap sizing: + product(bool, G1UseTimeBasedHeapSizing, false, EXPERIMENTAL, \ + "Enable time-based heap sizing to uncommit memory from inactive " \ + "regions independent of GC cycles") \ + \ + product(uintx, G1TimeBasedEvaluationIntervalMillis, 60000, MANAGEABLE, \ + "Interval in milliseconds between periodic heap size evaluations "\ + "when G1UseTimeBasedHeapSizing is enabled") \ + range(1000, max_uintx) \ + \ + product(uintx, G1UncommitDelayMillis, 300000, MANAGEABLE, \ + "A region is considered inactive if it has not been accessed " \ + "within this many milliseconds") \ + range(1000, max_uintx) \ + \ + product(size_t, G1MinRegionsToUncommit, 10, EXPERIMENTAL, \ + "Minimum number of inactive regions required before G1 will " \ + "attempt to uncommit memory") \ + range(1, max_uintx) \ + \ + // end of GC_G1_FLAGS #endif // SHARE_GC_G1_G1_GLOBALS_HPP diff --git a/test/hotspot/jtreg/gc/g1/TestG1RegionUncommit.java b/test/hotspot/jtreg/gc/g1/TestG1RegionUncommit.java new file mode 100644 index 00000000000..af8af7cf187 --- /dev/null +++ b/test/hotspot/jtreg/gc/g1/TestG1RegionUncommit.java @@ -0,0 +1,79 @@ +/* + * 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 + * @library /test/lib + * @modules java.base/jdk.internal.misc + * @run main/othervm -XX:+UseG1GC -Xms128m -Xmx512m -XX:G1UncommitDelay=1000 + * gc.g1.TestG1RegionUncommit + */ + +import java.util.ArrayList; +import jdk.test.lib.Utils; + +public class TestG1RegionUncommit { + private static final int allocSize = 64 * 1024 * 1024; // 64MB + private static volatile Object keepAlive; + + private static long getCommitted() { + return Runtime.getRuntime().totalMemory(); + } + + public static void main(String[] args) throws Exception { + // Initial allocation to force region commitment + System.out.println("Initial allocation"); + long beforeAlloc = getCommitted(); + keepAlive = new byte[allocSize]; + long afterAlloc = getCommitted(); + + // Free memory and wait for uncommit + System.out.println("Freeing memory"); + keepAlive = null; + System.gc(); + + // Wait longer than uncommit delay + Thread.sleep(2000); + + long afterUncommit = getCommitted(); + + // Verify uncommit occurred + System.out.println("Before allocation: " + beforeAlloc); + System.out.println("After allocation: " + afterAlloc); + System.out.println("After uncommit: " + afterUncommit); + + if (afterUncommit >= afterAlloc) { + throw new RuntimeException("Uncommit did not occur"); + } + + if (afterUncommit < beforeAlloc) { + throw new RuntimeException("Too much memory uncommitted"); + } + + System.out.println("Test passed!"); + } +} diff --git a/test/hotspot/jtreg/gc/g1/TestTimeBasedHeapConfig.java b/test/hotspot/jtreg/gc/g1/TestTimeBasedHeapConfig.java new file mode 100644 index 00000000000..571b044b872 --- /dev/null +++ b/test/hotspot/jtreg/gc/g1/TestTimeBasedHeapConfig.java @@ -0,0 +1,187 @@ +/* + * 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. + */ + +/** + * @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 TestTimeBasedHeapConfig + */ + +import java.util.*; +import java.lang.management.ManagementFactory; +import com.sun.management.HotSpotDiagnosticMXBean; +import com.sun.management.VMOption; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; + +public class TestTimeBasedHeapConfig { + + public static void main(String[] args) throws Exception { + testConfigurationParameters(); + testInvalidSettings(); + testDynamicUpdates(); + testBoundaryConditions(); + } + + /** + * Test various configuration parameter combinations + */ + static void testConfigurationParameters() throws Exception { + // Test default settings + verifyVMConfig(new String[] { + "-XX:+UseG1GC", + "-XX:+G1UseTimeBasedHeapSizing" + }); + + // Test custom evaluation interval + verifyVMConfig(new String[] { + "-XX:+UseG1GC", + "-XX:+G1UseTimeBasedHeapSizing", + "-XX:G1TimeBasedEvaluationIntervalMillis=30000" + }); + + // Test custom uncommit delay + verifyVMConfig(new String[] { + "-XX:+UseG1GC", + "-XX:+G1UseTimeBasedHeapSizing", + "-XX:G1UncommitDelayMillis=120000" + }); + + // Test custom region threshold + verifyVMConfig(new String[] { + "-XX:+UseG1GC", + "-XX:+G1UseTimeBasedHeapSizing", + "-XX:G1MinRegionsToUncommit=5" + }); + } + + /** + * Test invalid configuration settings + */ + static void testInvalidSettings() throws Exception { + // Test invalid evaluation interval + verifyVMErrorConfig(new String[] { + "-XX:+UseG1GC", + "-XX:+G1UseTimeBasedHeapSizing", + "-XX:G1TimeBasedEvaluationIntervalMillis=0" // Invalid + }); + + // Test invalid uncommit delay + verifyVMErrorConfig(new String[] { + "-XX:+UseG1GC", + "-XX:+G1UseTimeBasedHeapSizing", + "-XX:G1UncommitDelayMillis=100" // Too low + }); + + // Test invalid region count + verifyVMErrorConfig(new String[] { + "-XX:+UseG1GC", + "-XX:+G1UseTimeBasedHeapSizing", + "-XX:G1MinRegionsToUncommit=0" // Invalid + }); + } + + /** + * Test dynamic parameter updates + */ + static void testDynamicUpdates() throws Exception { + ProcessBuilder pb = ProcessTools.createTestJvm( + "-XX:+UseG1GC", + "-XX:+G1UseTimeBasedHeapSizing", + "TestTimeBasedHeapConfig$DynamicUpdateTest"); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + output.shouldHaveExitValue(0); + } + + /** + * Test boundary conditions + */ + static void testBoundaryConditions() throws Exception { + // Test minimum heap size + verifyVMConfig(new String[] { + "-XX:+UseG1GC", + "-XX:+G1UseTimeBasedHeapSizing", + "-Xms5m", // Very small initial heap + "-Xmx128m" + }); + + // Test maximum regions + verifyVMConfig(new String[] { + "-XX:+UseG1GC", + "-XX:+G1UseTimeBasedHeapSizing", + "-XX:G1MinRegionsToUncommit=100", // Large region count + "-Xmx2g" + }); + } + + private static void verifyVMConfig(String[] opts) throws Exception { + ProcessBuilder pb = ProcessTools.createTestJvm(opts); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + output.shouldHaveExitValue(0); + } + + private static void verifyVMErrorConfig(String[] opts) throws Exception { + ProcessBuilder pb = ProcessTools.createTestJvm(opts); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + output.shouldNotHaveExitValue(0); + } + + /** + * Tests dynamic parameter updates + */ + public static class DynamicUpdateTest { + private static final int MB = 1024 * 1024; + private static ArrayList arrays = new ArrayList<>(); + + public static void main(String[] args) throws Exception { + HotSpotDiagnosticMXBean diagnostic = ManagementFactory.getPlatformMXBean( + HotSpotDiagnosticMXBean.class); + + // Initial allocation + allocateMemory(100); + System.gc(); + + // Update parameters + diagnostic.setVMOption("G1TimeBasedEvaluationIntervalMillis", "45000"); + diagnostic.setVMOption("G1UncommitDelayMillis", "90000"); + + // More allocation + allocateMemory(50); + System.gc(); + + arrays = null; + System.gc(); + } + + static void allocateMemory(int mb) { + for (int i = 0; i < mb; i++) { + arrays.add(new byte[MB]); + } + } + } +} diff --git a/test/hotspot/jtreg/gc/g1/TestTimeBasedHeapSizing.java b/test/hotspot/jtreg/gc/g1/TestTimeBasedHeapSizing.java new file mode 100644 index 00000000000..58cbb3516d6 --- /dev/null +++ b/test/hotspot/jtreg/gc/g1/TestTimeBasedHeapSizing.java @@ -0,0 +1,362 @@ +/* + * 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. + */ + +/** + * @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:+G1UseTimeBasedHeapSizing TestTimeBasedHeapSizing + */ + +import java.util.*; +import java.lang.management.ManagementFactory; +import com.sun.management.HotSpotDiagnosticMXBean; +import com.sun.management.VMOption; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; + +public class TestTimeBasedHeapSizing { + + // Test configuration + private static final String TEST_VM_OPTS = "-XX:+UseG1GC " + + "-XX:+G1UseTimeBasedHeapSizing " + + "-XX:G1TimeBasedEvaluationIntervalMillis=30000 " + // 30 sec for testing + "-XX:G1UncommitDelayMillis=60000 " + // 1 min for testing + "-XX:G1MinRegionsToUncommit=2 " + // Lower for testing + "-Xmx1g -Xms512m " + // Start with some headroom + "-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xlog:gc*=debug"; + + public static void main(String[] args) throws Exception { + testBasicFunctionality(); + testHighLoadScenario(); + testIdleBehavior(); + testConcurrentGC(); + testErrorConditions(); + } + + /** + * Test basic functionality with default settings + */ + static void testBasicFunctionality() throws Exception { + ProcessBuilder pb = ProcessTools.createTestJvm(TEST_VM_OPTS, + "TestTimeBasedHeapSizing$BasicFunctionalityTest"); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + // Verify proper initialization + output.shouldContain("G1 periodic heap evaluation"); + + // Check for expected behavior + output.shouldContain("Time-based evaluation triggered"); + output.shouldMatch("Uncommit candidates: [0-9]+ regions"); + + output.shouldHaveExitValue(0); + } + + /** + * Test behavior under high allocation load with multiple threads + */ + static void testHighLoadScenario() throws Exception { + ProcessBuilder pb = ProcessTools.createTestJvm(TEST_VM_OPTS, + "TestTimeBasedHeapSizing$HighLoadTest"); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + // Verify proper handling of high allocation load + output.shouldContain("High allocation phase completed"); + output.shouldContain("Time-based evaluation"); + output.shouldNotContain("OutOfMemoryError"); + + output.shouldHaveExitValue(0); + } + + /** + * Test memory release during idle periods + */ + static void testIdleBehavior() throws Exception { + ProcessBuilder pb = ProcessTools.createTestJvm(TEST_VM_OPTS, + "TestTimeBasedHeapSizing$IdleBehaviorTest"); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + // Verify memory release during idle + output.shouldContain("Starting idle phase"); + output.shouldContain("Time-based evaluation triggered"); + output.shouldMatch("Uncommit candidates: [0-9]+ regions"); + output.shouldContain("Memory released"); + + output.shouldHaveExitValue(0); + } + + /** + * Test coordination with concurrent GC + */ + static void testConcurrentGC() throws Exception { + ProcessBuilder pb = ProcessTools.createTestJvm(TEST_VM_OPTS + + " -XX:InitiatingHeapOccupancyPercent=45", // Lower IHOP to trigger concurrent cycles + "TestTimeBasedHeapSizing$ConcurrentGCTest"); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + // Verify interaction with concurrent GC + output.shouldContain("Starting concurrent allocations"); + output.shouldContain("Concurrent Mark Cycle"); + output.shouldContain("Time-based evaluation"); + output.shouldNotContain("GC coordination error"); + + output.shouldHaveExitValue(0); + } + + /** + * Test error handling and recovery + */ + static void testErrorConditions() throws Exception { + ProcessBuilder pb = ProcessTools.createTestJvm(TEST_VM_OPTS + + " -XX:G1HeapRegionSize=1M", // Small regions for more edge cases + "TestTimeBasedHeapSizing$ErrorConditionsTest"); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + // Verify error handling + output.shouldContain("Starting error simulation"); + output.shouldContain("Time-based evaluation"); + output.shouldContain("Recovered from allocation failure"); + + output.shouldHaveExitValue(0); + } + + /** + * Base test class with common utilities + */ + static class BaseTest { + protected static final int MB = 1024 * 1024; + protected static ArrayList arrays = new ArrayList<>(); + + static void allocateMemory(int mb) { + for (int i = 0; i < mb; i++) { + arrays.add(new byte[MB]); + } + } + + static void clearMemory() { + arrays.clear(); + System.gc(); + } + } + + /** + * Basic functionality test + */ + 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 { + // Allocate some memory + allocateMemory(100); // 100MB + System.gc(); + + // Sleep to allow time-based evaluation + Thread.sleep(65000); // > G1UncommitDelayMillis + + // Verify heap size changes + System.gc(); + + // Clean up + arrays = null; + System.gc(); + } + + static void allocateMemory(int mb) { + for (int i = 0; i < mb; i++) { + arrays.add(new byte[MB]); + } + } + } + + /** + * High load test with multiple threads and varying allocation patterns + */ + public static class HighLoadTest extends BaseTest { + private static final int NUM_THREADS = 4; + private static final int ALLOCATION_SIZE_MB = 50; + private static final CountDownLatch startLatch = new CountDownLatch(1); + + public static void main(String[] args) throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS); + List> tasks = new ArrayList<>(); + + // Create allocation tasks + for (int i = 0; i < NUM_THREADS; i++) { + tasks.add(executor.submit(new AllocationTask())); + } + + System.out.println("Starting high allocation phase"); + startLatch.countDown(); + + // Wait for allocations to complete + for (Future task : tasks) { + task.get(); + } + + System.out.println("High allocation phase completed"); + executor.shutdown(); + + // Allow time for evaluation + Thread.sleep(65000); + + clearMemory(); + } + + static class AllocationTask implements Runnable { + public void run() { + try { + startLatch.await(); + ArrayList threadArrays = new ArrayList<>(); + + // Allocate and free memory in a loop + for (int i = 0; i < 3; i++) { + for (int j = 0; j < ALLOCATION_SIZE_MB; j++) { + threadArrays.add(new byte[MB]); + } + Thread.sleep(1000); + threadArrays.clear(); + System.gc(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + } + + /** + * Test memory release during idle periods + */ + public static class IdleBehaviorTest extends BaseTest { + public static void main(String[] args) throws Exception { + // Initial allocation + System.out.println("Initial allocation phase"); + allocateMemory(200); + System.gc(); + + // First idle period - shorter than uncommit delay + System.out.println("Starting short idle phase"); + clearMemory(); + Thread.sleep(30000); // < G1UncommitDelayMillis + + // Second allocation + allocateMemory(100); + System.gc(); + + // Long idle period - should trigger uncommit + System.out.println("Starting long idle phase"); + clearMemory(); + System.out.println("Starting idle phase"); + Thread.sleep(65000); // > G1UncommitDelayMillis + + System.out.println("Memory released"); + System.gc(); + } + } + + /** + * Test coordination with concurrent GC operations + */ + public static class ConcurrentGCTest extends BaseTest { + private static final int NUM_THREADS = 2; + + public static void main(String[] args) throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS); + CountDownLatch gcLatch = new CountDownLatch(1); + + // Start allocation thread + executor.submit(() -> { + try { + System.out.println("Starting concurrent allocations"); + while (!Thread.interrupted()) { + allocateMemory(10); + Thread.sleep(100); + clearMemory(); + } + } catch (InterruptedException e) { + // Expected + } + }); + + // Start GC thread + executor.submit(() -> { + try { + gcLatch.await(); + while (!Thread.interrupted()) { + System.gc(); + Thread.sleep(5000); + } + } catch (InterruptedException e) { + // Expected + } + }); + + // Let the threads run + gcLatch.countDown(); + Thread.sleep(65000); + + executor.shutdownNow(); + executor.awaitTermination(10, TimeUnit.SECONDS); + + clearMemory(); + } + } + + /** + * Test error handling and recovery scenarios + */ + public static class ErrorConditionsTest extends BaseTest { + public static void main(String[] args) throws Exception { + System.out.println("Starting error simulation"); + + // Rapid allocation/deallocation to stress region management + for (int i = 0; i < 5; i++) { + allocateMemory(100); + // Force fragmentation with small allocations + for (int j = 0; j < 1000; j++) { + arrays.add(new byte[1024]); // 1KB + } + clearMemory(); + Thread.sleep(100); + } + + // Large allocation followed by immediate clear + allocateMemory(400); + clearMemory(); + + // Sleep to allow uncommit + Thread.sleep(65000); + + // Verify we can still allocate + allocateMemory(50); + System.out.println("Recovered from allocation failure"); + + clearMemory(); + } + } +} diff --git a/test/hotspot/jtreg/gc/g1/TestTimeBasedRegionTracking.java b/test/hotspot/jtreg/gc/g1/TestTimeBasedRegionTracking.java new file mode 100644 index 00000000000..088eedd3cc6 --- /dev/null +++ b/test/hotspot/jtreg/gc/g1/TestTimeBasedRegionTracking.java @@ -0,0 +1,225 @@ +/* + * 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. + */ + +/** + * @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 -XX:+UseG1GC -XX:+G1UseTimeBasedHeapSizing TestTimeBasedRegionTracking + */ + +import java.util.*; +import java.lang.management.ManagementFactory; +import com.sun.management.HotSpotDiagnosticMXBean; +import com.sun.management.VMOption; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; + +public class TestTimeBasedRegionTracking { + + // Test configuration + private static final String TEST_VM_OPTS = "-XX:+UseG1GC " + + "-XX:+G1UseTimeBasedHeapSizing " + + "-XX:G1TimeBasedEvaluationIntervalMillis=15000 " + // 15s for testing + "-XX:G1UncommitDelayMillis=30000 " + // 30s for testing + "-XX:G1MinRegionsToUncommit=2 " + // Low for testing + "-Xmx1g -Xms512m " + // Start with headroom + "-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xlog:gc*=debug"; + + public static void main(String[] args) throws Exception { + testRegionStateTransitions(); + testConcurrentAllocation(); + testRegionReuse(); + } + + /** + * Test region state transitions through allocation/collection cycles + */ + static void testRegionStateTransitions() throws Exception { + ProcessBuilder pb = ProcessTools.createTestJvm(TEST_VM_OPTS, + "TestTimeBasedRegionTracking$RegionTransitionTest"); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + // Verify region state changes + output.shouldContain("Region state transition"); + output.shouldContain("Uncommit candidates found"); + + output.shouldHaveExitValue(0); + } + + /** + * Test region tracking during concurrent allocations + */ + static void testConcurrentAllocation() throws Exception { + ProcessBuilder pb = ProcessTools.createTestJvm(TEST_VM_OPTS, + "TestTimeBasedRegionTracking$ConcurrentAllocationTest"); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + // Verify concurrent allocation tracking + output.shouldContain("[gc,heap ] GC("); // GC log entry + output.shouldContain("[gc,phases ] Phase 1: Mark live objects"); + + output.shouldHaveExitValue(0); + } + + /** + * Test proper tracking when regions are reused + */ + static void testRegionReuse() throws Exception { + ProcessBuilder pb = ProcessTools.createTestJvm(TEST_VM_OPTS, + "TestTimeBasedRegionTracking$RegionReuseTest"); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + // Verify region reuse tracking + output.shouldContain("[gc,heap ] GC("); // GC log entry + output.shouldContain("[gc,phases ] Phase 2: Compute new object locations"); + + output.shouldHaveExitValue(0); + } + + /** + * Tests region state transitions + */ + 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 { + // Phase 1: Active allocation + allocateMemory(200); // 200MB + System.gc(); + + // Phase 2: Idle period + arrays.clear(); + System.gc(); + Thread.sleep(35000); // > G1UncommitDelayMillis + + // Phase 3: Reallocation + allocateMemory(100); // 100MB + System.gc(); + + // Clean up + arrays = null; + System.gc(); + } + + static void allocateMemory(int mb) { + for (int i = 0; i < mb; i++) { + arrays.add(new byte[MB]); + } + } + } + + /** + * Tests concurrent allocation behavior + */ + public static class ConcurrentAllocationTest { + private static final int MB = 1024 * 1024; + private static final int NUM_THREADS = 4; + private static volatile boolean running = true; + + static class AllocationThread extends Thread { + private final ArrayList arrays = new ArrayList<>(); + + @Override + public void run() { + try { + while (running) { + // Allocate 10MB per iteration + for (int i = 0; i < 10 && running; i++) { + arrays.add(new byte[MB]); + } + Thread.sleep(100); // Short pause between allocations + } + } catch (InterruptedException e) { + // Expected on shutdown + } + } + } + + public static void main(String[] args) throws Exception { + // Start concurrent allocation threads + Thread[] threads = new Thread[NUM_THREADS]; + for (int i = 0; i < NUM_THREADS; i++) { + threads[i] = new AllocationThread(); + threads[i].start(); + } + + // Let allocations run for a while + Thread.sleep(10000); + + // Trigger GC to observe region state tracking + System.gc(); + + // Stop allocation threads + running = false; + for (Thread t : threads) { + t.join(); + } + + // Final GC to clean up + System.gc(); + } + } + + /** + * Tests region reuse behavior and tracking + */ + public static class RegionReuseTest { + private static final int MB = 1024 * 1024; + private static ArrayList arrays = new ArrayList<>(); + + static void allocateAndCollect(int mbToAllocate) throws Exception { + // Allocate memory + for (int i = 0; i < mbToAllocate; i++) { + arrays.add(new byte[MB]); + } + + // Force a GC to trigger region reuse + System.gc(); + arrays.clear(); + + // Let regions become eligible for uncommit + Thread.sleep(35000); // > G1UncommitDelayMillis + } + + public static void main(String[] args) throws Exception { + // Phase 1: Initial allocation and collection + allocateAndCollect(200); // 200MB + + // Phase 2: Reallocate in same regions + allocateAndCollect(150); // 150MB + + // Phase 3: One more cycle with different size + allocateAndCollect(100); // 100MB + + // Clean up + arrays = null; + System.gc(); + } + } +} -- 2.49.0.windows.1