8373225: GenShen: More adaptive old-generation growth heuristics

Reviewed-by: wkemper, ysr
This commit is contained in:
Kelvin Nilsen 2025-12-12 14:02:35 +00:00
parent a05d5d2514
commit 410014377c
11 changed files with 158 additions and 59 deletions

View File

@ -104,7 +104,7 @@ void ShenandoahGenerationalHeuristics::choose_collection_set(ShenandoahCollectio
// Note that for GLOBAL GC, region may be OLD, and OLD regions do not qualify for pre-selection
// This region is old enough to be promoted but it was not preselected, either because its garbage is below
// ShenandoahOldGarbageThreshold so it will be promoted in place, or because there is not sufficient room
// old garbage threshold so it will be promoted in place, or because there is not sufficient room
// in old gen to hold the evacuated copies of this region's live data. In both cases, we choose not to
// place this region into the collection set.
if (region->get_top_before_promote() != nullptr) {

View File

@ -71,7 +71,8 @@ ShenandoahOldHeuristics::ShenandoahOldHeuristics(ShenandoahOldGeneration* genera
_growth_trigger(false),
_fragmentation_density(0.0),
_fragmentation_first_old_region(0),
_fragmentation_last_old_region(0)
_fragmentation_last_old_region(0),
_old_garbage_threshold(ShenandoahOldGarbageThreshold)
{
}
@ -373,7 +374,8 @@ void ShenandoahOldHeuristics::prepare_for_old_collections() {
}
}
_old_generation->set_live_bytes_after_last_mark(live_data);
// TODO: subtract from live_data bytes promoted during concurrent GC.
_old_generation->set_live_bytes_at_last_mark(live_data);
// Unlike young, we are more interested in efficiently packing OLD-gen than in reclaiming garbage first. We sort by live-data.
// Some regular regions may have been promoted in place with no garbage but also with very little live data. When we "compact"
@ -385,7 +387,7 @@ void ShenandoahOldHeuristics::prepare_for_old_collections() {
const size_t region_size_bytes = ShenandoahHeapRegion::region_size_bytes();
// The convention is to collect regions that have more than this amount of garbage.
const size_t garbage_threshold = region_size_bytes * ShenandoahOldGarbageThreshold / 100;
const size_t garbage_threshold = region_size_bytes * get_old_garbage_threshold() / 100;
// Enlightened interpretation: collect regions that have less than this amount of live.
const size_t live_threshold = region_size_bytes - garbage_threshold;
@ -655,6 +657,7 @@ bool ShenandoahOldHeuristics::should_start_gc() {
const double percent = percent_of(old_gen_capacity, heap_capacity);
log_trigger("Expansion failure, current size: %zu%s which is %.1f%% of total heap size",
byte_size_in_proper_unit(old_gen_capacity), proper_unit_for_byte_size(old_gen_capacity), percent);
adjust_old_garbage_threshold();
return true;
}
@ -677,6 +680,7 @@ bool ShenandoahOldHeuristics::should_start_gc() {
"%zu to %zu (%zu), density: %.1f%%",
byte_size_in_proper_unit(fragmented_free), proper_unit_for_byte_size(fragmented_free),
first_old_region, last_old_region, span_of_old_regions, density * 100);
adjust_old_garbage_threshold();
return true;
}
@ -699,12 +703,13 @@ bool ShenandoahOldHeuristics::should_start_gc() {
consecutive_young_cycles);
_growth_trigger = false;
} else if (current_usage > trigger_threshold) {
const size_t live_at_previous_old = _old_generation->get_live_bytes_after_last_mark();
const size_t live_at_previous_old = _old_generation->get_live_bytes_at_last_mark();
const double percent_growth = percent_of(current_usage - live_at_previous_old, live_at_previous_old);
log_trigger("Old has overgrown, live at end of previous OLD marking: "
"%zu%s, current usage: %zu%s, percent growth: %.1f%%",
byte_size_in_proper_unit(live_at_previous_old), proper_unit_for_byte_size(live_at_previous_old),
byte_size_in_proper_unit(current_usage), proper_unit_for_byte_size(current_usage), percent_growth);
adjust_old_garbage_threshold();
return true;
} else {
// Mixed evacuations have decreased current_usage such that old-growth trigger is no longer relevant.
@ -713,7 +718,41 @@ bool ShenandoahOldHeuristics::should_start_gc() {
}
// Otherwise, defer to inherited heuristic for gc trigger.
return this->ShenandoahHeuristics::should_start_gc();
bool result = this->ShenandoahHeuristics::should_start_gc();
if (result) {
adjust_old_garbage_threshold();
}
return result;
}
void ShenandoahOldHeuristics::adjust_old_garbage_threshold() {
const uintx MinimumOldGarbageThreshold = 10;
const uintx InterventionPercentage = 50;
const ShenandoahHeap* heap = ShenandoahHeap::heap();
size_t old_regions_size = _old_generation->used_regions_size();
size_t soft_max_size = heap->soft_max_capacity();
uintx percent_used = (uintx) ((old_regions_size * 100) / soft_max_size);
_old_garbage_threshold = ShenandoahOldGarbageThreshold;
if (percent_used > InterventionPercentage) {
uintx severity = percent_used - InterventionPercentage; // ranges from 0 to 50
if (MinimumOldGarbageThreshold < ShenandoahOldGarbageThreshold) {
uintx adjustment_potential = ShenandoahOldGarbageThreshold - MinimumOldGarbageThreshold;
// With default values:
// if percent_used > 80, garbage_threshold is 10
// else if percent_used > 65, garbage_threshold is 15
// else if percent_used > 50, garbage_threshold is 20
if (severity > 30) {
_old_garbage_threshold = ShenandoahOldGarbageThreshold - adjustment_potential;
} else if (severity > 15) {
_old_garbage_threshold = ShenandoahOldGarbageThreshold - 2 * adjustment_potential / 3;
} else {
_old_garbage_threshold = ShenandoahOldGarbageThreshold - adjustment_potential / 3;
}
log_info(gc)("Adjusting old garbage threshold to %lu because Old Generation used regions represents %lu%% of heap",
_old_garbage_threshold, percent_used);
}
}
}
void ShenandoahOldHeuristics::record_success_concurrent() {

View File

@ -102,6 +102,17 @@ private:
size_t _fragmentation_first_old_region;
size_t _fragmentation_last_old_region;
// The value of command-line argument ShenandoahOldGarbageThreshold represents the percent of garbage that must
// be present within an old-generation region before that region is considered a good candidate for inclusion in
// the collection set under normal circumstances. For our purposes, normal circustances are when the memory consumed
// by the old generation is less than 50% of the soft heap capacity. When the old generation grows beyond the 50%
// threshold, we dynamically adjust the old garbage threshold, allowing us to invest in packing the old generation
// more tightly so that more memory can be made available to the more frequent young GC cycles. This variable
// is used in place of ShenandoahOldGarbageThreshold. Under normal circumstances, its value is equal to
// ShenandoahOldGarbageThreshold. When the GC is under duress, this value may be adjusted to a smaller value,
// as scaled according to the severity of duress that we are experiencing.
uintx _old_garbage_threshold;
// Compare by live is used to prioritize compaction of old-gen regions. With old-gen compaction, the goal is
// to tightly pack long-lived objects into available regions. In most cases, there has not been an accumulation
// of garbage within old-gen regions. The more likely opportunity will be to combine multiple sparsely populated
@ -200,9 +211,28 @@ public:
bool is_experimental() override;
// Returns the current value of a dynamically adjusted threshold percentage of garbage above which an old region is
// deemed eligible for evacuation.
inline uintx get_old_garbage_threshold() { return _old_garbage_threshold; }
private:
void slide_pinned_regions_to_front();
bool all_candidates_are_pinned();
// The normal old_garbage_threshold is specified by ShenandoahOldGarbageThreshold command-line argument, with default
// value 25, denoting that a region that has at least 25% garbage is eligible for evacuation. With default values for
// all command-line arguments, we make the following adjustments:
// 1. If the old generation has grown to consume more than 80% of the soft max capacity, adjust threshold to 10%
// 2. Otherwise, if the old generation has grown to consume more than 65%, adjust threshold to 15%
// 3. Otherwise, if the old generation has grown to consume more than 50%, adjust threshold to 20%
// The effect is to compact the old generation more aggressively as the old generation consumes larger percentages
// of the available heap memory. In these circumstances, we pack the old generation more tightly in order to make
// more memory avaiable to the young generation so that the more frequent young collections can operate more
// efficiently.
//
// If the ShenandoahOldGarbageThreshold is specified on the command line, the effect of adjusting the old garbage
// threshold is scaled linearly.
void adjust_old_garbage_threshold();
};
#endif // SHARE_GC_SHENANDOAH_HEURISTICS_SHENANDOAHOLDHEURISTICS_HPP

View File

@ -505,10 +505,10 @@ inline void assert_no_in_place_promotions() {
#endif
}
// Preselect for inclusion into the collection set regions whose age is at or above tenure age which contain more than
// ShenandoahOldGarbageThreshold amounts of garbage. We identify these regions by setting the appropriate entry of
// the collection set's preselected regions array to true. All entries are initialized to false before calling this
// function.
// Preselect for inclusion into the collection set all regions whose age is at or above tenure age and for which the
// garbage percentage exceeds a dynamically adjusted threshold (known as the old-garbage threshold percentage). We
// identify these regions by setting the appropriate entry of the collection set's preselected regions array to true.
// All entries are initialized to false before calling this function.
//
// During the subsequent selection of the collection set, we give priority to these promotion set candidates.
// Without this prioritization, we found that the aged regions tend to be ignored because they typically have
@ -531,7 +531,8 @@ size_t ShenandoahGeneration::select_aged_regions(const size_t old_promotion_rese
bool* const candidate_regions_for_promotion_by_copy = heap->collection_set()->preselected_regions();
ShenandoahMarkingContext* const ctx = heap->marking_context();
const size_t old_garbage_threshold = (ShenandoahHeapRegion::region_size_bytes() * ShenandoahOldGarbageThreshold) / 100;
const size_t old_garbage_threshold =
(ShenandoahHeapRegion::region_size_bytes() * heap->old_generation()->heuristics()->get_old_garbage_threshold()) / 100;
const size_t pip_used_threshold = (ShenandoahHeapRegion::region_size_bytes() * ShenandoahGenerationalMinPIPUsage) / 100;

View File

@ -71,7 +71,7 @@ private:
// garbage-dense regions, including those that satisfy criteria 1 & 2 below,
// and whose live bytes will fit within old_available budget:
// Criterion 1. region age >= tenuring threshold
// Criterion 2. region garbage percentage > ShenandoahOldGarbageThreshold
// Criterion 2. region garbage percentage > old garbage threshold
//
// Identifies regions eligible for promotion in place,
// being those of at least tenuring_threshold age that have lower garbage

View File

@ -145,7 +145,7 @@ void ShenandoahGenerationalEvacuationTask::maybe_promote_region(ShenandoahHeapRe
// triggers the load-reference barrier (LRB) to copy on reference fetch.
//
// Aged humongous continuation regions are handled with their start region. If an aged regular region has
// more garbage than ShenandoahOldGarbageThreshold, we'll promote by evacuation. If there is room for evacuation
// more garbage than the old garbage threshold, we'll promote by evacuation. If there is room for evacuation
// in this cycle, the region will be in the collection set. If there is not room, the region will be promoted
// by evacuation in some future GC cycle.
@ -177,7 +177,8 @@ void ShenandoahGenerationalEvacuationTask::promote_in_place(ShenandoahHeapRegion
size_t region_size_bytes = ShenandoahHeapRegion::region_size_bytes();
{
const size_t old_garbage_threshold = (region_size_bytes * ShenandoahOldGarbageThreshold) / 100;
const size_t old_garbage_threshold =
(region_size_bytes * _heap->old_generation()->heuristics()->get_old_garbage_threshold()) / 100;
assert(!_heap->is_concurrent_old_mark_in_progress(), "Cannot promote in place during old marking");
assert(region->garbage_before_padded_for_promote() < old_garbage_threshold,
"Region %zu has too much garbage for promotion", region->index());

View File

@ -83,7 +83,7 @@ void ShenandoahGenerationalFullGC::handle_completion(ShenandoahHeap* heap) {
assert_usage_not_more_than_regions_used(young);
// Establish baseline for next old-has-grown trigger.
old->set_live_bytes_after_last_mark(old->used());
old->set_live_bytes_at_last_mark(old->used());
}
void ShenandoahGenerationalFullGC::rebuild_remembered_set(ShenandoahHeap* heap) {

View File

@ -116,11 +116,10 @@ ShenandoahOldGeneration::ShenandoahOldGeneration(uint max_queues)
_is_parsable(true),
_card_scan(nullptr),
_state(WAITING_FOR_BOOTSTRAP),
_growth_before_compaction(INITIAL_GROWTH_BEFORE_COMPACTION),
_min_growth_before_compaction ((ShenandoahMinOldGenGrowthPercent * FRACTIONAL_DENOMINATOR) / 100)
_growth_percent_before_collection(INITIAL_GROWTH_PERCENT_BEFORE_COLLECTION)
{
assert(type() == ShenandoahGenerationType::OLD, "OO sanity");
_live_bytes_after_last_mark = ShenandoahHeap::heap()->capacity() * INITIAL_LIVE_FRACTION / FRACTIONAL_DENOMINATOR;
_live_bytes_at_last_mark = (ShenandoahHeap::heap()->soft_max_capacity() * INITIAL_LIVE_PERCENT) / 100;
// Always clear references for old generation
ref_processor()->set_soft_reference_policy(true);
@ -221,20 +220,20 @@ ShenandoahOldGeneration::configure_plab_for_current_thread(const ShenandoahAlloc
}
}
size_t ShenandoahOldGeneration::get_live_bytes_after_last_mark() const {
return _live_bytes_after_last_mark;
size_t ShenandoahOldGeneration::get_live_bytes_at_last_mark() const {
return _live_bytes_at_last_mark;
}
void ShenandoahOldGeneration::set_live_bytes_after_last_mark(size_t bytes) {
void ShenandoahOldGeneration::set_live_bytes_at_last_mark(size_t bytes) {
if (bytes == 0) {
// Restart search for best old-gen size to the initial state
_live_bytes_after_last_mark = ShenandoahHeap::heap()->capacity() * INITIAL_LIVE_FRACTION / FRACTIONAL_DENOMINATOR;
_growth_before_compaction = INITIAL_GROWTH_BEFORE_COMPACTION;
_live_bytes_at_last_mark = (ShenandoahHeap::heap()->soft_max_capacity() * INITIAL_LIVE_PERCENT) / 100;
_growth_percent_before_collection = INITIAL_GROWTH_PERCENT_BEFORE_COLLECTION;
} else {
_live_bytes_after_last_mark = bytes;
_growth_before_compaction /= 2;
if (_growth_before_compaction < _min_growth_before_compaction) {
_growth_before_compaction = _min_growth_before_compaction;
_live_bytes_at_last_mark = bytes;
_growth_percent_before_collection /= 2;
if (_growth_percent_before_collection < ShenandoahMinOldGenGrowthPercent) {
_growth_percent_before_collection = ShenandoahMinOldGenGrowthPercent;
}
}
}
@ -244,7 +243,19 @@ void ShenandoahOldGeneration::handle_failed_transfer() {
}
size_t ShenandoahOldGeneration::usage_trigger_threshold() const {
size_t result = _live_bytes_after_last_mark + (_live_bytes_after_last_mark * _growth_before_compaction) / FRACTIONAL_DENOMINATOR;
size_t threshold_by_relative_growth =
_live_bytes_at_last_mark + (_live_bytes_at_last_mark * _growth_percent_before_collection) / 100;
size_t soft_max_capacity = ShenandoahHeap::heap()->soft_max_capacity();
size_t threshold_by_growth_into_percent_remaining;
if (_live_bytes_at_last_mark < soft_max_capacity) {
threshold_by_growth_into_percent_remaining = (size_t)
(_live_bytes_at_last_mark + ((soft_max_capacity - _live_bytes_at_last_mark)
* ShenandoahMinOldGenGrowthRemainingHeapPercent / 100.0));
} else {
// we're already consuming more than soft max capacity, so we should start old GC right away.
threshold_by_growth_into_percent_remaining = soft_max_capacity;
}
size_t result = MIN2(threshold_by_relative_growth, threshold_by_growth_into_percent_remaining);
return result;
}

View File

@ -287,28 +287,23 @@ public:
private:
State _state;
static const size_t FRACTIONAL_DENOMINATOR = 65536;
// During initialization of the JVM, we search for the correct old-gen size by initially performing old-gen
// collection when old-gen usage is 50% more (INITIAL_GROWTH_BEFORE_COMPACTION) than the initial old-gen size
// estimate (3.125% of heap). The next old-gen trigger occurs when old-gen grows 25% larger than its live
// memory at the end of the first old-gen collection. Then we trigger again when old-gen grows 12.5%
// more than its live memory at the end of the previous old-gen collection. Thereafter, we trigger each time
// old-gen grows more than 12.5% following the end of its previous old-gen collection.
static const size_t INITIAL_GROWTH_BEFORE_COMPACTION = FRACTIONAL_DENOMINATOR / 2; // 50.0%
// collection when old-gen usage is 50% more (INITIAL_GROWTH_PERCENT_BEFORE_COLLECTION) than the initial old-gen size
// estimate (16% of heap). With each successive old-gen collection, we divide the growth trigger by two, but
// never use a growth trigger smaller than ShenandoahMinOldGenGrowthPercent.
static const size_t INITIAL_GROWTH_PERCENT_BEFORE_COLLECTION = 50;
// INITIAL_LIVE_FRACTION represents the initial guess of how large old-gen should be. We estimate that old-gen
// needs to consume 6.25% of the total heap size. And we "pretend" that we start out with this amount of live
// INITIAL_LIVE_PERCENT represents the initial guess of how large old-gen should be. We estimate that old gen
// needs to consume 16% of the total heap size. And we "pretend" that we start out with this amount of live
// old-gen memory. The first old-collection trigger will occur when old-gen occupies 50% more than this initial
// approximation of the old-gen memory requirement, in other words when old-gen usage is 150% of 6.25%, which
// is 9.375% of the total heap size.
static const uint16_t INITIAL_LIVE_FRACTION = FRACTIONAL_DENOMINATOR / 16; // 6.25%
// approximation of the old-gen memory requirement, in other words when old-gen usage is 150% of 16%, which
// is 24% of the heap size.
static const size_t INITIAL_LIVE_PERCENT = 16;
size_t _live_bytes_after_last_mark;
size_t _live_bytes_at_last_mark;
// How much growth in usage before we trigger old collection, per FRACTIONAL_DENOMINATOR (65_536)
size_t _growth_before_compaction;
const size_t _min_growth_before_compaction; // Default is 12.5%
// How much growth in usage before we trigger old collection as a percent of soft_max_capacity
size_t _growth_percent_before_collection;
void validate_transition(State new_state) NOT_DEBUG_RETURN;
@ -323,8 +318,8 @@ public:
void transition_to(State new_state);
size_t get_live_bytes_after_last_mark() const;
void set_live_bytes_after_last_mark(size_t new_live);
size_t get_live_bytes_at_last_mark() const;
void set_live_bytes_at_last_mark(size_t new_live);
size_t usage_trigger_threshold() const;

View File

@ -59,15 +59,29 @@
"fail, resulting in stop-the-world full GCs.") \
range(0,100) \
\
product(double, ShenandoahMinOldGenGrowthPercent, 12.5, EXPERIMENTAL, \
product(double, ShenandoahMinOldGenGrowthPercent, 50, EXPERIMENTAL, \
"(Generational mode only) If the usage within old generation " \
"has grown by at least this percent of its live memory size " \
"at completion of the most recent old-generation marking " \
"effort, heuristics may trigger the start of a new old-gen " \
"collection.") \
"at the start of the previous old-generation marking effort, " \
"heuristics may trigger the start of a new old-gen collection.") \
range(0.0,100.0) \
\
product(uintx, ShenandoahIgnoreOldGrowthBelowPercentage,10, EXPERIMENTAL, \
product(double, ShenandoahMinOldGenGrowthRemainingHeapPercent, \
35, EXPERIMENTAL, \
"(Generational mode only) If the usage within old generation " \
"has grown to exceed this percent of the remaining heap that " \
"was not marked live within the old generation at the time " \
"of the last old-generation marking effort, heuristics may " \
"trigger the start of a new old-gen collection. Setting " \
"this value to a smaller value may cause back-to-back old " \
"generation marking triggers, since the typical memory used " \
"by the old generation is about 30% larger than the live " \
"memory contained within the old generation (because default " \
"value of ShenandoahOldGarbageThreshold is 25.") \
range(0.0,100.0) \
\
product(uintx, ShenandoahIgnoreOldGrowthBelowPercentage, \
40, EXPERIMENTAL, \
"(Generational mode only) If the total usage of the old " \
"generation is smaller than this percent, we do not trigger " \
"old gen collections even if old has grown, except when " \
@ -77,12 +91,13 @@
range(0,100) \
\
product(uintx, ShenandoahDoNotIgnoreGrowthAfterYoungCycles, \
50, EXPERIMENTAL, \
"(Generational mode only) Even if the usage of old generation " \
"is below ShenandoahIgnoreOldGrowthBelowPercentage, " \
"trigger an old-generation mark if old has grown and this " \
"many consecutive young-gen collections have been " \
"completed following the preceding old-gen collection.") \
100, EXPERIMENTAL, \
"(Generational mode only) Trigger an old-generation mark " \
"if old has grown and this many consecutive young-gen " \
"collections have been completed following the preceding " \
"old-gen collection. We perform this old-generation mark " \
"evvort even if the usage of old generation is below " \
"ShenandoahIgnoreOldGrowthBelowPercentage.") \
\
product(bool, ShenandoahGenerationalAdaptiveTenuring, true, EXPERIMENTAL, \
"(Generational mode only) Dynamically adapt tenuring age.") \

View File

@ -99,8 +99,12 @@ public class TestOldGrowthTriggers {
"-XX:+UnlockExperimentalVMOptions",
"-XX:+UseShenandoahGC",
"-XX:ShenandoahGCMode=generational",
"-XX:ShenandoahMinOldGenGrowthPercent=12.5",
"-XX:ShenandoahIgnoreOldGrowthBelowPercentage=10",
"-XX:ShenandoahMinOldGenGrowthRemainingHeapPercent=100",
"-XX:ShenandoahGuaranteedYoungGCInterval=0",
"-XX:ShenandoahGuaranteedOldGCInterval=0"
"-XX:ShenandoahGuaranteedOldGCInterval=0",
"-XX:-UseCompactObjectHeaders"
);
testOld("-Xlog:gc",
@ -110,6 +114,9 @@ public class TestOldGrowthTriggers {
"-XX:+UnlockExperimentalVMOptions",
"-XX:+UseShenandoahGC",
"-XX:ShenandoahGCMode=generational",
"-XX:ShenandoahMinOldGenGrowthPercent=12.5",
"-XX:ShenandoahIgnoreOldGrowthBelowPercentage=10",
"-XX:ShenandoahMinOldGenGrowthRemainingHeapPercent=100",
"-XX:ShenandoahGuaranteedYoungGCInterval=0",
"-XX:ShenandoahGuaranteedOldGCInterval=0",
"-XX:+UseCompactObjectHeaders"