Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 85 additions & 25 deletions src/hotspot/share/gc/g1/g1CollectedHeap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -467,8 +467,20 @@ HeapWord* G1CollectedHeap::attempt_allocation_slow(uint node_index, size_t word_
log_trace(gc, alloc)("%s: Unsuccessfully scheduled collection allocating %zu words",
Thread::current()->name(), word_size);

if (is_shutting_down()) {
stall_for_vm_shutdown();
return nullptr;
}

// Has the gc overhead limit been reached in the meantime? If so, this mutator
// should receive null even when unsuccessfully scheduling a collection as well
// for global consistency.
if (gc_overhead_limit_exceeded()) {
return nullptr;
}

// We can reach here if we were unsuccessful in scheduling a collection (because
// another thread beat us to it). In this case immeditealy retry the allocation
// another thread beat us to it). In this case immediately retry the allocation
// attempt because another thread successfully performed a collection and possibly
// reclaimed enough space. The first attempt (without holding the Heap_lock) is
// here and the follow-on attempt will be at the start of the next loop
Expand All @@ -485,11 +497,6 @@ HeapWord* G1CollectedHeap::attempt_allocation_slow(uint node_index, size_t word_
log_warning(gc, alloc)("%s: Retried allocation %u times for %zu words",
Thread::current()->name(), try_count, word_size);
}

if (is_shutting_down()) {
stall_for_vm_shutdown();
return nullptr;
}
}

ShouldNotReachHere();
Expand Down Expand Up @@ -714,6 +721,18 @@ HeapWord* G1CollectedHeap::attempt_allocation_humongous(size_t word_size) {
log_trace(gc, alloc)("%s: Unsuccessfully scheduled collection allocating %zu",
Thread::current()->name(), word_size);

if (is_shutting_down()) {
stall_for_vm_shutdown();
return nullptr;
}

// Has the gc overhead limit been reached in the meantime? If so, this mutator
// should receive null even when unsuccessfully scheduling a collection as well
// for global consistency.
if (gc_overhead_limit_exceeded()) {
return nullptr;
}

// We can reach here if we were unsuccessful in scheduling a collection (because
// another thread beat us to it).
// Humongous object allocation always needs a lock, so we wait for the retry
Expand All @@ -725,11 +744,6 @@ HeapWord* G1CollectedHeap::attempt_allocation_humongous(size_t word_size) {
log_warning(gc, alloc)("%s: Retried allocation %u times for %zu words",
Thread::current()->name(), try_count, word_size);
}

if (is_shutting_down()) {
stall_for_vm_shutdown();
return nullptr;
}
}

ShouldNotReachHere();
Expand Down Expand Up @@ -955,25 +969,62 @@ void G1CollectedHeap::resize_heap_after_young_collection(size_t allocation_word_
phase_times()->record_resize_heap_time((Ticks::now() - start).seconds() * 1000.0);
}

void G1CollectedHeap::update_gc_overhead_counter() {
assert(SafepointSynchronize::is_at_safepoint(), "precondition");

if (!UseGCOverheadLimit) {
return;
}

bool gc_time_over_limit = (_policy->analytics()->long_term_gc_time_ratio() * 100) >= GCTimeLimit;
double free_space_percent = percent_of(num_available_regions() * G1HeapRegion::GrainBytes, max_capacity());
bool free_space_below_limit = free_space_percent < GCHeapFreeLimit;

log_debug(gc)("GC Overhead Limit: GC Time %f Free Space %f Counter %zu",
(_policy->analytics()->long_term_gc_time_ratio() * 100),
free_space_percent,
_gc_overhead_counter);

if (gc_time_over_limit && free_space_below_limit) {
_gc_overhead_counter++;
} else {
_gc_overhead_counter = 0;
}
}

bool G1CollectedHeap::gc_overhead_limit_exceeded() {
return _gc_overhead_counter >= GCOverheadLimitThreshold;
}

HeapWord* G1CollectedHeap::satisfy_failed_allocation_helper(size_t word_size,
bool do_gc,
bool maximal_compaction,
bool expect_null_mutator_alloc_region) {
// Let's attempt the allocation first.
HeapWord* result =
attempt_allocation_at_safepoint(word_size,
expect_null_mutator_alloc_region);
if (result != nullptr) {
return result;
}
// Skip allocation if GC overhead limit has been exceeded to let the mutator run
// into an OOME. It can either exit "gracefully" or try to free up memory asap.
// For the latter situation, keep running GCs. If the mutator frees up enough
// memory quickly enough, the overhead(s) will go below the threshold(s) again
// and the VM may continue running.
// If we did not continue garbage collections, the (gc overhead) limit may decrease
// enough by itself to not count as exceeding the limit any more, in the worst
// case bouncing back-and-forth all the time.
if (!gc_overhead_limit_exceeded()) {
// Let's attempt the allocation first.
HeapWord* result =
attempt_allocation_at_safepoint(word_size,
expect_null_mutator_alloc_region);
if (result != nullptr) {
return result;
}

// In a G1 heap, we're supposed to keep allocation from failing by
// incremental pauses. Therefore, at least for now, we'll favor
// expansion over collection. (This might change in the future if we can
// do something smarter than full collection to satisfy a failed alloc.)
result = expand_and_allocate(word_size);
if (result != nullptr) {
return result;
// In a G1 heap, we're supposed to keep allocation from failing by
// incremental pauses. Therefore, at least for now, we'll favor
// expansion over collection. (This might change in the future if we can
// do something smarter than full collection to satisfy a failed alloc.)
result = expand_and_allocate(word_size);
if (result != nullptr) {
return result;
}
}

if (do_gc) {
Expand All @@ -997,6 +1048,10 @@ HeapWord* G1CollectedHeap::satisfy_failed_allocation_helper(size_t word_size,
HeapWord* G1CollectedHeap::satisfy_failed_allocation(size_t word_size) {
assert_at_safepoint_on_vm_thread();

// Update GC overhead limits after the initial garbage collection leading to this
// allocation attempt.
update_gc_overhead_counter();

// Attempts to allocate followed by Full GC.
HeapWord* result =
satisfy_failed_allocation_helper(word_size,
Expand Down Expand Up @@ -1028,6 +1083,10 @@ HeapWord* G1CollectedHeap::satisfy_failed_allocation(size_t word_size) {
return result;
}

if (gc_overhead_limit_exceeded()) {
log_info(gc)("GC Overhead Limit exceeded too often (%zu).", GCOverheadLimitThreshold);
}

// What else? We might try synchronous finalization later. If the total
// space available is large enough for the allocation, then a more
// complete compaction phase than we've tried so far might be
Expand Down Expand Up @@ -1209,6 +1268,7 @@ class HumongousRegionSetChecker : public G1HeapRegionSetChecker {

G1CollectedHeap::G1CollectedHeap() :
CollectedHeap(),
_gc_overhead_counter(0),
_service_thread(nullptr),
_periodic_gc_task(nullptr),
_free_arena_memory_task(nullptr),
Expand Down
11 changes: 11 additions & 0 deletions src/hotspot/share/gc/g1/g1CollectedHeap.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,17 @@ class G1CollectedHeap : public CollectedHeap {
friend class G1CheckRegionAttrTableClosure;

private:
// GC Overhead Limit functionality related members.
//
// The goal is to return null for allocations prematurely (before really going
// OOME) in case both GC CPU usage (>= GCTimeLimit) and not much available free
// memory (<= GCHeapFreeLimit) so that applications can exit gracefully or try
// to keep running by easing off memory.
uintx _gc_overhead_counter; // The number of consecutive garbage collections we were over the limits.

void update_gc_overhead_counter();
bool gc_overhead_limit_exceeded();

G1ServiceThread* _service_thread;
G1ServiceTask* _periodic_gc_task;
G1MonotonicArenaFreeMemoryTask* _free_arena_memory_task;
Expand Down
5 changes: 0 additions & 5 deletions src/hotspot/share/gc/parallel/parallelArguments.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,6 @@ void ParallelArguments::initialize() {
}
}

// True in product build, since tests using debug build often stress GC
if (FLAG_IS_DEFAULT(UseGCOverheadLimit)) {
FLAG_SET_DEFAULT(UseGCOverheadLimit, trueInProduct);
}

if (InitialSurvivorRatio < MinSurvivorRatio) {
if (FLAG_IS_CMDLINE(InitialSurvivorRatio)) {
if (FLAG_IS_CMDLINE(MinSurvivorRatio)) {
Expand Down
9 changes: 8 additions & 1 deletion src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,13 @@ bool ParallelScavengeHeap::check_gc_overhead_limit() {
bool little_mutator_time = _size_policy->mutator_time_percent() * 100 < (100 - GCTimeLimit);
bool little_free_space = check_gc_heap_free_limit(_young_gen->free_in_bytes(), _young_gen->capacity_in_bytes())
&& check_gc_heap_free_limit( _old_gen->free_in_bytes(), _old_gen->capacity_in_bytes());

log_debug(gc)("GC Overhead Limit: GC Time %f Free Space Young %f Old %f Counter %zu",
(100 - _size_policy->mutator_time_percent()),
percent_of(_young_gen->free_in_bytes(), _young_gen->capacity_in_bytes()),
percent_of(_old_gen->free_in_bytes(), _young_gen->capacity_in_bytes()),
_gc_overhead_counter);

if (little_mutator_time && little_free_space) {
_gc_overhead_counter++;
if (_gc_overhead_counter >= GCOverheadLimitThreshold) {
Expand Down Expand Up @@ -426,7 +433,7 @@ HeapWord* ParallelScavengeHeap::satisfy_failed_allocation(size_t size, bool is_t
}

if (check_gc_overhead_limit()) {
log_info(gc)("GCOverheadLimitThreshold %zu reached.", GCOverheadLimitThreshold);
log_info(gc)("GC Overhead Limit exceeded too often (%zu).", GCOverheadLimitThreshold);
return nullptr;
}

Expand Down
2 changes: 1 addition & 1 deletion src/hotspot/share/gc/parallel/parallelScavengeHeap.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class ParallelScavengeHeap : public CollectedHeap {

WorkerThreads _workers;

uint _gc_overhead_counter;
uintx _gc_overhead_counter;

bool _is_heap_almost_full;

Expand Down
2 changes: 1 addition & 1 deletion src/hotspot/share/gc/shared/gc_globals.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@
"Initial ratio of young generation/survivor space size") \
range(3, max_uintx) \
\
product(bool, UseGCOverheadLimit, true, \
product(bool, UseGCOverheadLimit, falseInDebug, \
"Use policy to limit of proportion of time spent in GC " \
"before an OutOfMemory error is thrown") \
\
Expand Down
98 changes: 98 additions & 0 deletions test/hotspot/jtreg/gc/TestUseGCOverheadLimit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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;

/*
* @test id=Parallel
* @requires vm.gc.Parallel
* @requires !vm.debug
* @summary Verifies that the UseGCOverheadLimit functionality works in Parallel GC.
* @library /test/lib
* @run driver gc.TestUseGCOverheadLimit Parallel
*/

/*
* @test id=G1
* @requires vm.gc.G1
* @requires !vm.debug
* @summary Verifies that the UseGCOverheadLimit functionality works in G1 GC.
* @library /test/lib
* @run driver gc.TestUseGCOverheadLimit G1
*/

import java.util.Arrays;
import java.util.stream.Stream;

import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;

public class TestUseGCOverheadLimit {
public static void main(String args[]) throws Exception {
String[] parallelArgs = {
"-XX:+UseParallelGC",
"-XX:NewSize=122m",
"-XX:SurvivorRatio=99",
"-XX:GCHeapFreeLimit=10"
};
String[] g1Args = {
"-XX:+UseG1GC",
"-XX:GCHeapFreeLimit=5"
};

String[] selectedArgs = args[0].equals("G1") ? g1Args : parallelArgs;

final String[] commonArgs = {
"-XX:-UseCompactObjectHeaders", // Object sizes are calculated such that the heap is tight.
"-XX:ParallelGCThreads=1", // Make GCs take longer.
"-XX:+UseGCOverheadLimit",
"-Xlog:gc=debug",
"-XX:GCTimeLimit=90", // Ease the CPU requirement a little.
"-Xmx128m",
Allocating.class.getName()
};

String[] vmArgs = Stream.concat(Arrays.stream(selectedArgs), Arrays.stream(commonArgs)).toArray(String[]::new);
OutputAnalyzer output = ProcessTools.executeLimitedTestJava(vmArgs);
output.shouldNotHaveExitValue(0);

System.out.println(output.getStdout());

output.stdoutShouldContain("GC Overhead Limit exceeded too often (5).");
}

static class Allocating {
public static void main(String[] args) {
Object[] cache = new Object[1024 * 1024 * 2];

// Allocate random objects, keeping around data, causing garbage
// collections.
for (int i = 0; i < 1024* 1024 * 30; i++) {
Object[] obj = new Object[10];
cache[i % cache.length] = obj;
}

System.out.println(cache);
}
}
}