Unity IL2CPP 垃圾回收
源码位置
Unity IL2cpp使用的是BoehmGC,源码位置在
Unity\Editor\Data\il2cpp\external
前提
熟悉内存管理Unity IL2cpp内存管理
GC_Collect流程
- GC_gcollect
GC_API void GC_CALL GC_gcollect(void)
{
/* 0 is passed as stop_func to get GC_default_stop_func value */
/* while holding the allocation lock (to prevent data races). */
(void)GC_try_to_collect_general(0, FALSE);
if (GC_have_errors) GC_print_all_errors();
}
- GC_try_to_collect_general
/* If stop_func == 0 then GC_default_stop_func is used instead. */
STATIC GC_bool GC_try_to_collect_general(GC_stop_func stop_func,
GC_bool force_unmap GC_ATTR_UNUSED)
{
GC_bool result;
IF_USE_MUNMAP(int old_unmap_threshold;)
IF_CANCEL(int cancel_state;)
DCL_LOCK_STATE;
if (!EXPECT(GC_is_initialized, TRUE)) GC_init();
if (GC_debugging_started) GC_print_all_smashed();
GC_INVOKE_FINALIZERS();
LOCK();
DISABLE_CANCEL(cancel_state);
# ifdef USE_MUNMAP
old_unmap_threshold = GC_unmap_threshold;
if (force_unmap ||
(GC_force_unmap_on_gcollect && old_unmap_threshold > 0))
GC_unmap_threshold = 1; /* unmap as much as possible */
# endif
ENTER_GC();
/* Minimize junk left in my registers */
GC_noop6(0,0,0,0,0,0);
result = GC_try_to_collect_inner(stop_func != 0 ? stop_func :
GC_default_stop_func);
EXIT_GC();
IF_USE_MUNMAP(GC_unmap_threshold = old_unmap_threshold); /* restore */
RESTORE_CANCEL(cancel_state);
UNLOCK();
if (result) {
if (GC_debugging_started) GC_print_all_smashed();
GC_INVOKE_FINALIZERS();
}
return(result);
}
-
GC_try_to_collect_inner
GC_INNER GC_bool GC_try_to_collect_inner(GC_stop_func stop_func) { # ifndef NO_CLOCK CLOCK_TYPE start_time = 0; /* initialized to prevent warning. */ GC_bool start_time_valid; # endif ASSERT_CANCEL_DISABLED(); GC_ASSERT(I_HOLD_LOCK()); if (GC_dont_gc || (*stop_func)()) return FALSE; if (GC_on_collection_event) GC_on_collection_event(GC_EVENT_START); if (GC_incremental && GC_collection_in_progress()) { GC_COND_LOG_PRINTF( "GC_try_to_collect_inner: finishing collection in progress\n"); /* Just finish collection already in progress. */ while(GC_collection_in_progress()) { if ((*stop_func)()) { /* TODO: Notify GC_EVENT_ABANDON */ return(FALSE); } GC_collect_a_little_inner(1); } } GC_notify_full_gc(); # ifndef NO_CLOCK start_time_valid = FALSE; if ((GC_print_stats | (int)measure_performance) != 0) { if (GC_print_stats) GC_log_printf("Initiating full world-stop collection!\n"); start_time_valid = TRUE; GET_TIME(start_time); } # endif GC_promote_black_lists(); /* Make sure all blocks have been reclaimed, so sweep routines */ /* don't see cleared mark bits. */ /* If we're guaranteed to finish, then this is unnecessary. */ /* In the find_leak case, we have to finish to guarantee that */ /* previously unmarked objects are not reported as leaks. */ # ifdef PARALLEL_MARK if (GC_parallel) GC_wait_for_reclaim(); # endif if ((GC_find_leak || stop_func != GC_never_stop_func) && !GC_reclaim_all(stop_func, FALSE)) { /* Aborted. So far everything is still consistent. */ /* TODO: Notify GC_EVENT_ABANDON */ return(FALSE); } GC_invalidate_mark_state(); /* Flush mark stack. */ GC_clear_marks(); # ifdef SAVE_CALL_CHAIN GC_save_callers(GC_last_stack); # endif GC_is_full_gc = TRUE; if (!GC_stopped_mark(stop_func)) { if (!GC_incremental) { /* We're partially done and have no way to complete or use */ /* current work. Reestablish invariants as cheaply as */ /* possible. */ GC_invalidate_mark_state(); GC_unpromote_black_lists(); } /* else we claim the world is already still consistent. We'll */ /* finish incrementally. */ /* TODO: Notify GC_EVENT_ABANDON */ return(FALSE); } GC_finish_collection(); # ifndef NO_CLOCK if (start_time_valid) { CLOCK_TYPE current_time; unsigned long time_diff; GET_TIME(current_time); time_diff = MS_TIME_DIFF(current_time, start_time); if (measure_performance) full_gc_total_time += time_diff; /* may wrap */ if (GC_print_stats) GC_log_printf("Complete collection took %lu msecs\n", time_diff); } # endif if (GC_on_collection_event) GC_on_collection_event(GC_EVENT_END); return(TRUE); }
- 13-24行
if (GC_incremental && GC_collection_in_progress()) {
GC_COND_LOG_PRINTF(
"GC_try_to_collect_inner: finishing collection in progress\n");
/* Just finish collection already in progress. */
while(GC_collection_in_progress()) {
if ((*stop_func)()) {
/* TODO: Notify GC_EVENT_ABANDON */
return(FALSE);
}
GC_collect_a_little_inner(1);
}
}
如果是增量,且在垃圾回收过程中,把当前周期一直跑完。所以从这儿看,在增量垃圾回收的时候,调用gc.collect会耗时更严重。
- GC_clear_marks 清理所有block标记。hb_n_marks=0,hb_marks=0;
- GC_stopped_mark是标记过程。
- GC_finish_collection是完成了垃圾回收。
标记
-
GC_stopped_mark主要是以下 工作:
- 准备工作,暂停线程,
- 从根节点开始扫描,包括栈内存、寄存器变量、静态数据区的内存开始遍历,判断根节点内存地址指向的内存空间是否位于一个有效的hblk内存块中。将这些有效的内存空间标记起来,并暂存到一个临时数组中。
- 进一步扫描临时数组中的所有对象的内存空间,判断其指向的内存空间是否位于有效的hblk内存块中,并进行标记。直到全部遍历结束。
- 结束标记流程,恢复线程。
-
根节点是怎么来的?
-
GC_push_roots函数实现,包含了:
- GC_static_roots。void il2cpp::gc::GarbageCollector::RegisterRoot(char *start, size_t size)提供了方法可以注册过来
- GC_push_regs_and_stack,寄存器和栈
-
-
GC_initiate_gc
- GC_mark_state = MS_PUSH_RESCUERS;
-
GC_mark_some
- GC_approx_sp很奇怪,是取了1个局部变量的地址。用这个来取到当前栈的顶地址
- Push阶段,把used block ,uncollectable,栈顶地址(GC_approx_sp提供)等放入statck,作为标记的root
-
标记阶段,核心函数GC_mark_from,遍历statck(不是程序栈)。
- 从开始地址,按照指针大小,遍历内存
-
把内存内容作为1个地址,获取Header。寻找Header的Hash算法,可以让一个block里的地址,算出来是一样的hash值,最多偏差1个block.再根据block的起始地址和大小,判断具体是那个block.
-
如果获取成功
-
成功,再看地址是否对齐该block的hb_sz
- 对齐,就标记hb_marks
- 不对齐,丢弃
- 不成功,丢弃
-
-
- 当stack遍历完,返回.
-
回收阶段
全量和增量差异
全量是GC_mark_some,直到state = none
增量式执行若干GC_mark_some,单次每次都要挂起线程,恢复线程。
FAQ
-
增量标记阶段,如果修改了已扫描的对象,怎么办?
- IL2CPP_ENABLE_WRITE_BARRIERS生效的话,通过GarbageCollector::SetWriteBarrier来告诉垃圾回收引用变量,需要重新扫描。
- IL2CPP_ENABLE_WRITE_BARRIERS不生效的话,VirtualAlloc时设置MEM_WRITE_WATCH,GetWriteWatch就可以拿到哪些地址被修改过了
-
为什么之前发现c#的thread下载比native的慢很多
- 因为线程在增量标记下,老是被挂起,之前多线程下载比Native慢很多,重要原因之一。
参考
https://tech.bytedance.net/articles/6942348528106618894#heading10
https://zhuanlan.zhihu.com/p/379371712
https://www.hboehm.info/gc/gcdescr.html