# 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 至主堆分配器,会慢很多,造成卡顿
- 分帧处理
- 有源码加大堆栈内存
- 在我们想要分配内存时,Unity 会分配两块内存:Header (记录如 (1) 是否使用 (2) User 大小 (3) 前一块是谁)+User
Batch Allocator
:SRPDynamicHeapAllocator
:主分配器- ...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 来对比,具体性能差异当然是不可而知了