diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp index 169384c3a3710..83d4a71a77e0b 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp @@ -2072,6 +2072,11 @@ size_t G1CollectedHeap::max_capacity() const { return max_regions() * G1HeapRegion::GrainBytes; } +size_t G1CollectedHeap::soft_max_capacity() const { + return clamp(align_up(Atomic::load(&SoftMaxHeapSize), HeapAlignment), MinHeapSize, + max_capacity()); +} + void G1CollectedHeap::prepare_for_verify() { _verifier->prepare_for_verify(); } diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp index 92e08e6fc610b..ed5904e6f2ef0 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp @@ -1197,9 +1197,12 @@ class G1CollectedHeap : public CollectedHeap { // requires. static size_t humongous_obj_size_in_regions(size_t word_size); - // Print the maximum heap capacity. + // Returns the maximum heap capacity. size_t max_capacity() const override; + // Returns the soft maximum heap capacity. + size_t soft_max_capacity() const; + Tickspan time_since_last_collection() const { return Ticks::now() - _collection_pause_end; } // Convenience function to be used in situations where the heap type can be diff --git a/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp b/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp index c35ce7c356d28..a793ad406d65e 100644 --- a/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp +++ b/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp @@ -55,8 +55,8 @@ double G1HeapSizingPolicy::scale_with_heap(double pause_time_threshold) { // If the heap is at less than half its maximum size, scale the threshold down, // to a limit of 1%. Thus the smaller the heap is, the more likely it is to expand, // though the scaling code will likely keep the increase small. - if (_g1h->capacity() <= _g1h->max_capacity() / 2) { - threshold *= (double)_g1h->capacity() / (double)(_g1h->max_capacity() / 2); + if (_g1h->capacity() <= _g1h->soft_max_capacity() / 2) { + threshold *= (double)_g1h->capacity() / (double)(_g1h->soft_max_capacity() / 2); threshold = MAX2(threshold, 0.01); } @@ -91,8 +91,9 @@ size_t G1HeapSizingPolicy::young_collection_expansion_amount() { double threshold = scale_with_heap(pause_time_threshold); size_t expand_bytes = 0; + size_t soft_max_capacity = _g1h->soft_max_capacity(); - if (_g1h->capacity() == _g1h->max_capacity()) { + if (_g1h->capacity() >= soft_max_capacity) { log_expansion(short_term_pause_time_ratio, long_term_pause_time_ratio, threshold, pause_time_threshold, true, 0); clear_ratio_check_data(); @@ -123,9 +124,8 @@ size_t G1HeapSizingPolicy::young_collection_expansion_amount() { if ((_ratio_over_threshold_count == MinOverThresholdForGrowth) || (filled_history_buffer && (long_term_pause_time_ratio > threshold))) { size_t min_expand_bytes = G1HeapRegion::GrainBytes; - size_t reserved_bytes = _g1h->max_capacity(); size_t committed_bytes = _g1h->capacity(); - size_t uncommitted_bytes = reserved_bytes - committed_bytes; + size_t uncommitted_bytes = soft_max_capacity - committed_bytes; size_t expand_bytes_via_pct = uncommitted_bytes * G1ExpandByPercentOfAvailable / 100; double scale_factor = 1.0; @@ -243,14 +243,14 @@ size_t G1HeapSizingPolicy::full_collection_resize_amount(bool& expand) { "maximum_desired_capacity = %zu", minimum_desired_capacity, maximum_desired_capacity); - // Should not be greater than the heap max size. No need to adjust + size_t soft_max_capacity = _g1h->soft_max_capacity(); + // Should not be greater than the soft max capacity. No need to adjust // it with respect to the heap min size as it's a lower bound (i.e., // we'll try to make the capacity larger than it, not smaller). - minimum_desired_capacity = MIN2(minimum_desired_capacity, MaxHeapSize); - // Should not be less than the heap min size. No need to adjust it - // with respect to the heap max size as it's an upper bound (i.e., - // we'll try to make the capacity smaller than it, not greater). - maximum_desired_capacity = MAX2(maximum_desired_capacity, MinHeapSize); + minimum_desired_capacity = MIN2(minimum_desired_capacity, soft_max_capacity); + // Should not be less than the heap min size, and should not exceed + // the soft max capacity. + maximum_desired_capacity = clamp(maximum_desired_capacity, MinHeapSize, soft_max_capacity); // Don't expand unless it's significant; prefer expansion to shrinking. if (capacity_after_gc < minimum_desired_capacity) { diff --git a/src/hotspot/share/gc/g1/g1IHOPControl.cpp b/src/hotspot/share/gc/g1/g1IHOPControl.cpp index 5c05169c29ded..aacff5ad38882 100644 --- a/src/hotspot/share/gc/g1/g1IHOPControl.cpp +++ b/src/hotspot/share/gc/g1/g1IHOPControl.cpp @@ -50,6 +50,12 @@ void G1IHOPControl::update_allocation_info(double allocation_time_s, size_t addi _last_allocation_time_s = allocation_time_s; } +size_t G1IHOPControl::default_conc_mark_start_threshold() { + guarantee(_target_occupancy > 0, "Target occupancy must have been initialized."); + size_t actual_target_occupancy = MIN2(G1CollectedHeap::heap()->soft_max_capacity(), _target_occupancy); + return (size_t) (_initial_ihop_percent * actual_target_occupancy / 100.0); +} + void G1IHOPControl::print() { assert(_target_occupancy > 0, "Target occupancy still not updated yet."); size_t cur_conc_mark_start_threshold = get_conc_mark_start_threshold(); @@ -108,8 +114,9 @@ size_t G1AdaptiveIHOPControl::actual_target_threshold() const { double safe_total_heap_percentage = MIN2((double)(_heap_reserve_percent + _heap_waste_percent), 100.0); - return (size_t)MIN2( + return (size_t)MIN3( G1CollectedHeap::heap()->max_capacity() * (100.0 - safe_total_heap_percentage) / 100.0, + static_cast(G1CollectedHeap::heap()->soft_max_capacity()), _target_occupancy * (100.0 - _heap_waste_percent) / 100.0 ); } @@ -142,7 +149,7 @@ size_t G1AdaptiveIHOPControl::get_conc_mark_start_threshold() { return predicted_initiating_threshold; } else { // Use the initial value. - return (size_t)(_initial_ihop_percent * _target_occupancy / 100.0); + return default_conc_mark_start_threshold(); } } diff --git a/src/hotspot/share/gc/g1/g1IHOPControl.hpp b/src/hotspot/share/gc/g1/g1IHOPControl.hpp index 507fbb269d1d6..e078d10093001 100644 --- a/src/hotspot/share/gc/g1/g1IHOPControl.hpp +++ b/src/hotspot/share/gc/g1/g1IHOPControl.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 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 @@ -55,6 +55,10 @@ class G1IHOPControl : public CHeapObj { // Most recent time from the end of the concurrent start to the start of the first // mixed gc. virtual double last_marking_length_s() const = 0; + + // Default non-young occupancy at which concurrent marking should start. + size_t default_conc_mark_start_threshold(); + public: virtual ~G1IHOPControl() { } @@ -92,8 +96,7 @@ class G1StaticIHOPControl : public G1IHOPControl { G1StaticIHOPControl(double ihop_percent, G1OldGenAllocationTracker const* old_gen_alloc_tracker); size_t get_conc_mark_start_threshold() { - guarantee(_target_occupancy > 0, "Target occupancy must have been initialized."); - return (size_t) (_initial_ihop_percent * _target_occupancy / 100.0); + return default_conc_mark_start_threshold(); } virtual void update_marking_length(double marking_length_s) { diff --git a/test/hotspot/jtreg/gc/g1/TestSoftMaxHeapSize.java b/test/hotspot/jtreg/gc/g1/TestSoftMaxHeapSize.java new file mode 100644 index 0000000000000..ee4563f118bb5 --- /dev/null +++ b/test/hotspot/jtreg/gc/g1/TestSoftMaxHeapSize.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025, Google LLC. 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 + * @bug 8236073 + * @requires vm.gc.G1 + * @requires vm.opt.ExplicitGCInvokesConcurrent != true + * @library /test/lib + * @run main/othervm -Xmx256m -XX:MinHeapSize=4m -XX:MinHeapFreeRatio=99 + -XX:MaxHeapFreeRatio=99 gc.g1.TestSoftMaxHeapSize + * @summary SoftMaxHeapSize should limit G1's heap size when resizing. + */ + +import java.lang.management.ManagementFactory; +import java.util.LinkedList; +import java.util.List; +import jdk.test.lib.dcmd.PidJcmdExecutor; + +public class TestSoftMaxHeapSize { + + private static final int OBJECT_SIZE = 1000; + private static final long ALLOCATED_BYTES = 20_000_000; // About 20M + + // Uses power-of-two value so it is aligned and is the actual effective value. + private static final long SOFT_MAX_HEAP = + 32 * 1024 * 1024; // 32MiB, leaving ~12MiB headroom above ALLOCATED_BYTES. + + private static final List holder = new LinkedList<>(); + + private static long getCurrentHeapSize() { + return ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getCommitted(); + } + + public static void main(String[] args) throws Exception { + + long count = ALLOCATED_BYTES / OBJECT_SIZE; + for (long i = 0; i < count; ++i) { + holder.add(new byte[OBJECT_SIZE]); + } + + System.gc(); + long heapSize = getCurrentHeapSize(); + if (heapSize != ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax()) { + throw new RuntimeException( + "Heap size did not fully expand to Xmx after full GC: heapSize = " + heapSize); + } + + PidJcmdExecutor jcmd = new PidJcmdExecutor(); + jcmd.execute("VM.set_flag SoftMaxHeapSize " + SOFT_MAX_HEAP, true); + + System.gc(); + heapSize = getCurrentHeapSize(); + if (heapSize != SOFT_MAX_HEAP) { + throw new RuntimeException( + "Heap size did not shrink to SoftMaxHeapSize after full GC: heapSize = " + heapSize); + } + } +} diff --git a/test/hotspot/jtreg/gc/g1/TestSoftMaxHeapSizeNoOOM.java b/test/hotspot/jtreg/gc/g1/TestSoftMaxHeapSizeNoOOM.java new file mode 100644 index 0000000000000..4011deecbf798 --- /dev/null +++ b/test/hotspot/jtreg/gc/g1/TestSoftMaxHeapSizeNoOOM.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025, Google LLC. 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 id=normal + * @bug 8236073 8352765 + * @requires vm.gc.G1 + * @library /test/lib + * @run main/othervm -Xmx100m -XX:MinHeapSize=4m -XX:SoftMaxHeapSize=4m + gc.g1.TestSoftMaxHeapSizeNoOOM + * @summary Setting SoftMaxHeapSize to a small value won't trigger + * OutOfMemoryError for normal allocations. + */ + +/* + * @test id=humongous + * @bug 8236073 8352765 + * @requires vm.gc.G1 + * @library /test/lib + * @run main/othervm -Xmx100m -XX:MinHeapSize=4m -XX:SoftMaxHeapSize=4m + -Dhumongous=true gc.g1.TestSoftMaxHeapSizeNoOOM + * @summary Setting SoftMaxHeapSize to a small value won't trigger + * OutOfMemoryError for humongous allocations. + */ + +import java.util.ArrayList; + +public class TestSoftMaxHeapSizeNoOOM { + + private static final long ALLOCATED_BYTES = 20_000_000; // About 20M + private static final int OBJECT_SIZE = 1000; + private static final int ITERATIONS = 100000; + private static final int HUMONGOUS_OBJECT_SIZE = 1_500_000; // About 1.5M + private static final int HUMONGOUS_ITERATIONS = 1000; + + private static final ArrayList holder = new ArrayList<>(); + + private static void work(int objSize, int iterations) { + long count = ALLOCATED_BYTES / objSize; + for (long i = 0; i < count; ++i) { + holder.add(new byte[objSize]); + } + // Mutate old objects while allocating new objects. + // This is effective to trigger concurrent collections for G1, + // and is necessary to reproduce OutOfMemoryError in JDK-8352765. + for (long i = 0; i < iterations; ++i) { + holder.remove(0); + holder.add(new byte[objSize]); + } + } + + public static void main(String[] args) throws Exception { + if (Boolean.getBoolean("humongous")) { + work(HUMONGOUS_OBJECT_SIZE, HUMONGOUS_ITERATIONS); + } else { + work(OBJECT_SIZE, ITERATIONS); + } + } +}