Last active
April 14, 2024 07:12
-
-
Save hashbrowncipher/010d8b6fa458676fde84a8b48a1825d3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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