Skip to content

Instantly share code, notes, and snippets.

@hashbrowncipher
Last active April 14, 2024 07:12
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save hashbrowncipher/010d8b6fa458676fde84a8b48a1825d3 to your computer and use it in GitHub Desktop.
Save hashbrowncipher/010d8b6fa458676fde84a8b48a1825d3 to your computer and use it in GitHub Desktop.
Eden Sizing
There are limits to the size that Eden is allowed to be. Those limits are calculated in calculate_default_min_length:
G1YoungGenSizer::recalculate_min_max_young_length -> G1YoungGenSizer::calculate_default_min_length
G1CollectorPolicy::update_young_list_target_length is responsible for
determining the actual size of the young list. If doing a young GC, it will
call G1CollectorPolicy::calculate_young_list_target_length. This function
attempts to find the largest eden size that will allow (predicted) GC pauses to
remain under the pause time target. Presumably after a certain eden size, there
is too much for a given young collection to clean up in eden.
http://stackoverflow.com/questions/36345409/why-does-g1gc-shrink-the-young-generation-before-starting-mixed-collections
All of this is different in mixed collections. It appears that mixed GCs make
Eden as small as possible, attempt to clean up the tenured generation, and then
get back to "normal" operation.
uint young_list_target_length = 0;
if (gcs_are_young()) {
young_list_target_length =
calculate_young_list_target_length(rs_lengths,
base_min_length,
desired_min_length,
desired_max_length);
_rs_lengths_prediction = rs_lengths;
} else {
// Don't calculate anything and let the code below bound it to
// the desired_min_length, i.e., do the next GC as soon as
// possible to maximize how many old regions we can add to it.
}
The comment there is indicative: apparently G1 wants frequent GCs during mixed
collections. The (allocation rate / size of Eden) is directly proportional to
GC frequency, so G1GC chooses a small young gen. Once young_list_target_length
is chosen, the JVM applies its static rules (like G1NewSizePercent). So we
should expect a 5% young gen during mixed collections.
Backpressure
G1GC has "Concurrent Refinement Threads", which are apparently instrumental for
identifying garbage in mixed collections. Mutator threads (i.e. Java code)
create "dirty cards", which are placed in a queue. Based on the queue size,
G1GC launches some number of threads to process the queue. Initially, no
threads process the queue: the work is deferred until a STW pause. When the
number of items reaches the threshold of "the green zone"
(G1ConcRefinementGreenZone), the JVM begins creating threads. Once there are
G1ConcRefinementYellowZone (default: 2*G1ConcRefinementGreenZone) items in the
queue, all threads are active. The number of threads active increases
linearly(?) as the number of queue items progresses through the green zone. All
refinement threads remain active up to the G1ConcRefinementRedZone (default:
3*G1ConcRefinementYellowZone) threshold. In the red zone, the refinement
threads are considered to be "falling behind", and the JVM forces threads
adding to the queue to do the G1 refinement work themselves.
The red zone provides backpressure, preventing the refinement threads from
falling too far behind. This is good! There is an adaptive tuning algorithm
which adjusts the zone thresholds based on how much time was spent doing
refinement during STW pauses (controlled by G1UseAdaptiveConcRefinement,
default true). On each STW pause, if refinement took longer than a goal amount
(G1RSetUpdatingPauseTimePercent, default 10) of the total G1 pause goal time
(default 200ms), the thresholds are adjusted down (by 10%). If it took less
than the goal amount, the thresholds are adjusted up (by 1.1x).
const int k_gy = 3, k_gr = 6;
const double inc_k = 1.1, dec_k = 0.9;
int g = cg1r->green_zone();
if (update_rs_time > goal_ms) {
g = (int)(g * dec_k); // Can become 0, that's OK. That would mean a mutator-only processing.
} else {
if (update_rs_time < goal_ms && update_rs_processed_buffers > g) {
g = (int)MAX2(g * inc_k, g + 1.0);
}
}
// Change the refinement threads params
cg1r->set_green_zone(g);
cg1r->set_yellow_zone(g * k_gy);
cg1r->set_red_zone(g * k_gr);
cg1r->reinitialize_threads();
My problem with the above code is that (to mine eyes), it does not include any
limits. If allocation conditions change, then this can only react to the tune
of 10% on each GC pause. Given that I can find no limits on how high the
thresholds can go when the goal is met repeatedly, this means that the heap can
be 100% full with garbage without all of the concurrent refinement threads even
running. As far as I can tell, this could mean that backpressure fails to work
entirely, causing heap fillup and a single threaded Full GC. It may be better
to disable G1UseAdaptiveConcRefinement instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment