Repository navigation
g1 evacuation Mixed_GC_Condition
ceragon edited this page Dec 27, 2021
·
7 revisions
以下方法是 Mixed GC 是否触发的唯一判断方法,大致可以描述为:当回收集合内的垃圾对象占总 heap 空间的比值超过 G1HeapWastePercent 的时候会返回真
bool G1Policy::next_gc_should_be_mixed(const char* true_action_str, const char* false_action_str) const {
// 已选择的集合是否为空
if (cset_chooser()->is_empty()) {
return false;
}
// 统计出的可回收的对象的总字节数 (cset 内的可回收对象)
size_t reclaimable_bytes = cset_chooser()->remaining_reclaimable_bytes();
// 这个值表示可回收率, 可回收率 = 可回收字节数 / 已激活 region 区域的容量总和
double reclaimable_percent = reclaimable_bytes_percent(reclaimable_bytes);
double threshold = (double) G1HeapWastePercent;
// threshold 默认值是 5, 可回收率小于 5% 则不需要进入 Mixed GC
if (reclaimable_percent <= threshold) {
return false;
}
return true;
}上面的方法涉及到了 cset 集合,这是一个待回收的老年代 region 集合.下面就来看下这个集合的增加和减少流程吧.
- 下面方法是添加老年代 region 到 cset 中. 其实添加 region 还有另外一个多线程的方法,但我只想探究 Mixed GC 的触发条件,所以那个方法暂不考虑
class CollectionSetChooser: public CHeapObj<mtGC> {
GrowableArray<HeapRegion*> _regions;
uint _end;
size_t _remaining_reclaimable_bytes;
};
void CollectionSetChooser::add_region(HeapRegion* hr) {
// 放入集合中
_regions.append(hr);
// 集合扩充
_end++;
// 统计所有 region 的可回收字节数
_remaining_reclaimable_bytes += hr->reclaimable_bytes();
}- 继续追踪调用
add_region的位置,下面方法没啥好说的
class CSetChooserParUpdater : public StackObj {
void add_region(HeapRegion* hr) {
_chooser->add_region(hr);
}
};- 继续查看上面方法的调用,在此处出现了分支条件,部分解读详见附录
class ParKnownGarbageHRClosure: public HeapRegionClosure {
bool do_heap_region(HeapRegion* r) {
// 解读在附录的 should_add() 中。&& 当前不是能正处于被分配的状态中。
if (_cset_updater.should_add(r) && !_g1h->is_old_gc_alloc_region(r)) {
// 如果满足条件,则添加 region 到 cset 中
_cset_updater.add_region(r);
} else if (r->is_old()) {
r->rem_set()->clear(true /* only_cardset */);
}
return false;
}
};- 由于上面的
do_heap_region方法在一个闭包中,在 jdk 源码中遇到闭包,通常需要看闭包的实例化位置.下面方法对该闭包进行了实例化
class ParKnownGarbageTask: public AbstractGangTask {
void work(uint worker_id) {
// 闭包实例化
ParKnownGarbageHRClosure par_known_garbage_cl(_hrSorted, _chunk_size);
// 以下方法可以不用深入了解,可以理解为就是遍历一部分 region,并调用上面的 do_heap_region 方法
_g1h->heap_region_par_iterate_from_worker_offset(&par_known_garbage_cl, &_hrclaimer, worker_id);
}
};- 上面的方法其其实是一个并行线程池的 Task,该线程池内的线程都会看到这个 Task并调用 work 方法.而下面的方法就是发布一个并行任务
void CollectionSetChooser::rebuild(WorkGang* workers, uint n_regions) {
clear();
uint n_workers = workers->active_workers();
// 计算每个工人处理多少个 region
uint chunk_size = calculate_parallel_work_chunk_size(n_workers, n_regions);
// Task 实例化
ParKnownGarbageTask par_known_garbage_task(this, chunk_size, n_workers);
// 开始并行任务
workers->run_task(&par_known_garbage_task);
// 此时 cset 就填充完毕了, 那么对 cset 进行排序.详细解读详见附录
sort_regions();
}- 继续查看 rebuild 方法的调用, 可以看到组装完 cset 后,就执行了 Mixed GC 的触发检查.从下面的
_g1h->workers(),进行查询赋值确认该线程是 GC Thread ,所以上面的 Task 实际上是在 GC Thread 中执行的.
void G1Policy::record_concurrent_mark_cleanup_end() {
// num_regions() 取到的是当前 heap 已使用的总 region 个数
cset_chooser()->rebuild(_g1h->workers(), _g1h->num_regions());
// 这个就是本文开头提到的 Mixed GC 的触发条件方法
bool mixed_gc_pending = next_gc_should_be_mixed("request mixed gcs", "request young-only gcs");
// 设置下一次 GC 是不是 Mixed GC
collector_state()->set_in_young_gc_before_mixed(mixed_gc_pending);
}- 我还想查看一下上面方法的根源是哪里,所以又继续追查了一下.发现是被并发标记的最后一个阶段 cleanup 方法调用的
void G1ConcurrentMark::cleanup() {
_g1h->g1_policy()->record_concurrent_mark_cleanup_end();
}- 那 cleanup 是在哪里执行的呢,书上说是 STW 下运行,咱们接着往后看
class CMCleanup : public VoidClosure {
void do_void(){
_cm->cleanup();
}
};- 可以看到上面的方法是一个闭包的内部方法,所以应该看闭包的实例化位置,下面的方法对闭包进行了实例化.
- run_service 方法,它是 G1 Main Marker 线程的主循环.所以可以理解为上面的过程都是被这个线程所触发的.
- VM Thread 主要作用就是发起 STW 申请,并在 STW 到来的时候执行指定的任务.
- 调用过程就是
G1 Main Marker->VM Thread->GC Thread
void G1ConcurrentMarkThread::run_service() {
while (!should_terminate()) {
// 清理的闭包实例化
CMCleanup cl_cl(_cm);
// 实例化一个 VM Thread 线程的操作实例,最终会在 VM Thread 线程中执行,并且是在 STW 后执行上面的 do_void 方法
VM_CGC_Operation op(&cl_cl, "Pause Cleanup");
// 在 VM Thread 中执行操作
VMThread::execute(&op);
}
}简而言之,应该添加的是老年代的 region,且 region 内的存活对象空间占用比例必须低于 85%
bool CollectionSetChooser::should_add(HeapRegion* hr) const {
return !hr->is_young() && // 非年轻代
!hr->is_pinned() && // 非固定区域(例如:大对象 region)
region_occupancy_low_enough_for_evac(hr->live_bytes()) && // region 内的存活对象空间占用低于阈值
hr->rem_set()->is_complete();// remember set 不能正在更新
}
bool CollectionSetChooser::region_occupancy_low_enough_for_evac(size_t live_bytes) {
// 存活对象的空间占用小于阈值才会纳入回收集合
return live_bytes < mixed_gc_live_threshold_bytes();
}
static size_t mixed_gc_live_threshold_bytes() {
// G1MixedGCLiveThresholdPercent 默认值 85。假设 region 大小是 1Mb,返回值是 891289 = 870Kb
return HeapRegion::GrainBytes * (size_t) G1MixedGCLiveThresholdPercent / 100;
}- 以下是 cset 的排序方法,和 java 中的 sort 很类似,需要关注 order_regions 这个方法的内容
void CollectionSetChooser::sort_regions() {
_regions.sort(order_regions);
}- 下面的方法就是排序的规则
static int order_regions(HeapRegion** hr1p, HeapRegion** hr2p) {
return order_regions(*hr1p, *hr2p);
}
static int order_regions(HeapRegion* hr1, HeapRegion* hr2) {
// 获取 region 的回收价值,价值高的排前面
double gc_eff1 = hr1->gc_efficiency();
double gc_eff2 = hr2->gc_efficiency();
if (gc_eff1 > gc_eff2) {
return -1;
} if (gc_eff1 < gc_eff2) {
return 1;
} else {
return 0;
}
}- 接着看下 gc_efficiency() 这个方法,由于这个方法用到了 _gc_efficiency 变量,所以直接看这个变量的赋值过程。可以看到可回收空间越多,转移花费的时间越少,则这个 region 的回收价值就越高.
double HeapRegion::gc_efficiency() {
// 就是返回一个变量,下面的方法是赋值
return _gc_efficiency;
}
void HeapRegion::calc_gc_efficiency() {
G1CollectedHeap* g1h = G1CollectedHeap::heap();
G1Policy* g1p = g1h->g1_policy();
// 根据历史经验,计算出这个 region 预计的转移时间
double region_elapsed_time_ms = g1p->predict_region_elapsed_time_ms(this, false /* for_young_gc */);
// reclaimable_bytes() 是在获取这个 region 内的可回收字节数
_gc_efficiency = (double) reclaimable_bytes() / region_elapsed_time_ms;
}- 下面的方法是从 cset 中删除 region 的唯一方法,
HeapRegion* pop() {
HeapRegion* hr = regions_at(_front);
regions_at_put(_front, NULL);
// 从总的可回收空间中减去被移除的 region 所占的空间
_remaining_reclaimable_bytes -= hr->reclaimable_bytes();
// 首索引自增
_front += 1;
return hr;
}- 下面方法是删除 region 的唯一方法,会在 Mixed GC 主逻辑中执行.总的 STW 时间是限定好的,之前回收 Young Region 已经花费掉一部分时间了,参数
time_remaining_ms是留给 Old Region 回收的时间.
void G1CollectionSet::finalize_old_part(double time_remaining_ms) {
// 必须处于 mixed gc 中
if (collector_state()->in_mixed_phase()) {
// 当前 cset 元素个数的 8% 为下限
const uint min_old_cset_length = _policy->calc_min_old_cset_length();
// 当前 heap 内已激活 region 总数的 10% 为上限
const uint max_old_cset_length = _policy->calc_max_old_cset_length();
// 从 cset 中获取一个 region(实际上是从排好序的 list 中获取第一个 region)
HeapRegion* hr = cset_chooser()->peek();
while (hr != NULL) {
// 每一次 mix gc 时,纳入到回收范围的老年代 region 有个数量上限
if (old_region_length() >= max_old_cset_length) {
break;
}
// 剩余待回收的空间大小
size_t reclaimable_bytes = cset_chooser()->remaining_reclaimable_bytes();
// 可回收率
double reclaimable_percent = _policy->reclaimable_bytes_percent(reclaimable_bytes);
// 可回收率的阈值
double threshold = (double) G1HeapWastePercent;
if (reclaimable_percent <= threshold) {
// 低于阈值就不用继续添加 region 了
break;
}
// 根据历史经验预测一下这个 region 回收需要花费的时间
double predicted_time_ms = predict_region_elapsed_time_ms(hr);
// 只要年轻代的空间是自适应的,这里就是 true
if (check_time_remaining) {
// 剩余的暂停时间是否足够回收这个 region
if (predicted_time_ms > time_remaining_ms) {
// 但是只要本次回收的 region 不够最小数量,也需要继续循环
if (old_region_length() >= min_old_cset_length) {
break;
}
}
} else {
// 本次回收的 region 是否满足最小数量的要求
if (old_region_length() >= min_old_cset_length) {
break;
}
}
// 满足了放入回收列表的要求,所以从 cset 中移除
cset_chooser()->pop();
// 再从 cset 获取下一个 region
hr = cset_chooser()->peek();
}
}
}回收率的算法是: cset 内可回收的空间除以 heap 已激活 reigon 的总空间
double G1Policy::reclaimable_bytes_percent(size_t reclaimable_bytes) const {
return percent_of(reclaimable_bytes, _g1h->capacity());
}- 标记结束时,如果可回收空间率超过 5% 就会触发 Mixed GC
- 每次 Mixed GC 选择全部的 young region 和 部分 old region 进行回收
- 当可回收空间率低于 5% 时,Mixed GC 停止