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

posted on 2023-02-16 16:31  marcher  阅读(323)  评论(0编辑  收藏  举报

导航