# Native Mamery

Unity 如何分配内存?

  • New/Malloc:
    • 一个是函数一个是操作符,原理上讲 操作符 快于 函数
    • 分配失败 New 会抛出 Exception,Malloc 返回 null
    • Malloc 实现都类似,New 各有不同
      • 例如构造函数,New/Delete 会调用对象的 构造函数 / 析构函数 以完成对象的 构造 / 析构。而 Malloc/Free 则不会
      • 但是 New 并不代表是 Malloc + 构造函数
    • New 内存分配于 FreeStore (自由存储区【C++ 基于 new 操作符的一个抽象概念】),Malloc 会分配于 Heap 上
      • New/Delete 时,是否分配或释放内存,New 可以由自己决定,Malloc/Free 一定会返还系统
    • 所以严格讲两者根本不是一个东西,其实没有关系

Unity 不会直接通过 New/Malloc 进行操作,而是自己通过宏实现了一套内存分配机制,比如 UNITY_MALLOC ,分配内存时,会给内存一个标识符 ( Memery Label ),提交到 Memery Manager 进行分配。

  • 像 Profiler 中 Take Sample ,用以区分运行时每块内存所属,就是通过 Memery Label 区分。
  • Memery Label 同时也可以帮助 Memery Manager 做一个筛选,区分应该通过哪种内存分配器策略分配内存
  • 主要内存分配器:
    • Stack Allocator :快速、容量小、临时对象分配器

      注:图上的 Heap 是内存对象的 Heap,Stack Allocator 指先进后出结构
      • :对于栈内存,默认就会预分配一块,然后通过栈顶指针在内存块中移动,此时不会真的重新申请内存,所以快 —— 官方测试比动态堆分配器大概快 3~5 倍
      • :分配多了很可能会被浪费,而且申请后就不会释放,是永远占用的
      • 临时 :上述机制也代表栈顶不可能长时间不动,快速收缩膨胀,高效重复利用。所以常驻对象不大行。
        • 在我们想要分配内存时,Unity 会分配两块内存:Header (记录如 (1) 是否使用 (2) User 大小 (3) 前一块是谁)+User
          • 所以会有额外消耗,例如自己想分配 16 字节内存,可能最终会有 32 字节大小
        • 回收中间对象时,Header 直接置为 『已删除』
        • 栈顶指针回弹时,会检测回弹指针位置对象是否已经 『已删除』,若是则再次往上移动,直到一个没有被标记为删除的块,或挪到了整个 Block 头
        • 栈结构分配虽然快,但是若栈顶对象未释放,中间内存即使回收也无法复用
          • 它必须等待栈顶指针对象被释放,才能回弹检测连续内存
          • 所以无法快速重用中间已释放内存
        • 栈内存大小有限制 (虽然会进行拓展)
          • Editor:主线程有 16M,Woker 线程 256KB
          • Runtime:主线程 128KB~1MB,Woker 线程 64KB
        • 栈内存爆了之后:Memery.FallbackAllocation,Fallback 至主堆分配器,会慢很多,造成卡顿
          • 分帧处理
          • 有源码加大堆栈内存
    • Batch Allocator :SRP
    • DynamicHeapAllocator :主分配器
    • ...15~20 种不同的分配器,每一种适用的场景不同

# Managed Memery

  • Mono:保留内存不会返给系统
  • Il2CPP:保留内存可能返给系统 —— 在同一个『页』6 次 GC 都没有被触及到的情况,很难 (注意不是对象,是内存管理单位),Unity 确实设置了这一个机制,但是很难达成。
  • 注意内存碎片化带来的保留内存上升
  • Unity 使用 Boehm GC (保守式 GC):
    • 一级列表 :将对象分为不同类型(如 PTRFREE (无指针类型)、Normal (一般类型)、不可回收类型 (回收器自己用))

    • 二级列表 :表明当前类型下的内存块大小 (16 字节增量,最多到 2K)

    • 二层 链表 :每一个大小内存块为一个链表,逻辑链接起来

      • 当用户需求分配一个内存对象,就会拿出指定大小的第一个块返给用户
      • 若用户需要分配大小小于最低大小,会返回最低大小的一个 Block (例如最小 16 字节,用户需求 8 字节,返回的就是 16 字节的内存块,会造成浪费)
      • 若所需大小不足,则向更高级取:然后将高级大小分拆分为两半,一半返回用户、另一半链到低级链表上
      • 同时进行回收时,若两个节点物理地址连接,则其会试图将两者合并,挂到更大的节点下,从而尽量减少整体碎片化 —— 注意不是移动内存,而是直接挂指针
    • 回收一个 Object 内存块时,会尝试找到这个内存卡块下所有指针指向地址,并且标记为引用
      以一个不可回收对象 ObjectA 为例,需要同时标记其引用对象也不可回收。但是在内存层次上因为已经没有 class 信息,无法确认对象地址存储的是值还是指针,靠『猜测 (pattern)』判断,因此叫『潜在指针』

      • 因此它总是假设给定值是指针,并且将相关联的对象标记为存活状态:
        • 若指向对象,则将对象也标记为不可回收
        • 若正好指向不相干的对象,不相干的对象也会被标记为不可回收 (不相干的对象指恰好被分配到这个地址上的对象,与其无实际依赖关系)
        • 若正好指向空白内存,空白内存地址将会加入黑名单,下次分配时若刚好踩到该地址,将不可分配内存
  • Boehm GC 问题:
    • 不分代、不合并 (整理内存),容易导致内存碎片
      • 例如当我们保留内存还剩很多,但是突然又被分了一大块就是这种情况
    • 非精准回收
      • 已分配内存在无人引用时,不一定能收回
      • 没有分配使用的内存,当想要分配使用时不一定能使用
  • 其它:
    • GC 回收方式有:Boehm GC (保守式)、S-Gen GC (分代式)、引用式 GC (Java)
    • Unity 继续使用保守式 Boehm GC,也有觉得移动平台再额外花费 CPU 去整理内存是不合算的事情缘故
      • 当然,由于没有实装 S-Gen GC 来对比,具体性能差异当然是不可而知了