Skip to content

g1 evacuation Mixed_GC_Condition

ceragon edited this page Dec 27, 2021 · 7 revisions

Mixed GC 触发条件

条件

以下方法是 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 集合

上面的方法涉及到了 cset 集合,这是一个待回收的老年代 region 集合.下面就来看下这个集合的增加和减少流程吧.

添加 region

  1. 下面方法是添加老年代 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();
}
  1. 继续追踪调用 add_region的位置,下面方法没啥好说的
class CSetChooserParUpdater : public StackObj {
    void add_region(HeapRegion* hr) {
        _chooser->add_region(hr);
    }
};
  1. 继续查看上面方法的调用,在此处出现了分支条件,部分解读详见附录
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;
    }
};
  1. 由于上面的 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);
    }
};
  1. 上面的方法其其实是一个并行线程池的 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();
}
  1. 继续查看 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);
}
  1. 我还想查看一下上面方法的根源是哪里,所以又继续追查了一下.发现是被并发标记的最后一个阶段 cleanup 方法调用的
void G1ConcurrentMark::cleanup() {
    _g1h->g1_policy()->record_concurrent_mark_cleanup_end();
}
  1. 那 cleanup 是在哪里执行的呢,书上说是 STW 下运行,咱们接着往后看
class CMCleanup : public VoidClosure {
    void do_void(){
        _cm->cleanup();
    }
};
  1. 可以看到上面的方法是一个闭包的内部方法,所以应该看闭包的实例化位置,下面的方法对闭包进行了实例化.
    • 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);    
    }
}

附录

should_add()

简而言之,应该添加的是老年代的 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;
}
sort_regions()
  1. 以下是 cset 的排序方法,和 java 中的 sort 很类似,需要关注 order_regions 这个方法的内容
void CollectionSetChooser::sort_regions() {
  _regions.sort(order_regions);
}
  1. 下面的方法就是排序的规则
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;
  }
}
  1. 接着看下 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;
}

删除 region

  1. 下面的方法是从 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;
}
  1. 下面方法是删除 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();
        }
    }
}

附录

reclaimable_bytes_percent()

回收率的算法是: cset 内可回收的空间除以 heap 已激活 reigon 的总空间

double G1Policy::reclaimable_bytes_percent(size_t reclaimable_bytes) const {
    return percent_of(reclaimable_bytes, _g1h->capacity());
}

结论

  1. 标记结束时,如果可回收空间率超过 5% 就会触发 Mixed GC
  2. 每次 Mixed GC 选择全部的 young region 和 部分 old region 进行回收
  3. 当可回收空间率低于 5% 时,Mixed GC 停止

Clone this wiki locally