1. Unity 的自动内存管理
- Unity 内部有两个内存管理池: 堆内存和堆栈内存. 堆栈内存 (stack) 主要用来存储较小的和短暂的数据, 堆内存 (heap) 主要用来存储较大的和存储时间较长的数据.
- Unity 中的变量只会在堆栈或者堆内存上进行内存分配, 变量要么存储在堆栈内存上, 要么处于堆内存上
- 只要变量处于激活状态, 则其占用的内存会被标记为使用状态, 则该部分的内存处于被分配的状态.
- 一旦变量不再激活, 则其所占用的内存不再需要, 该部分内存可以被回收到内存池中被再次使用, 这样的操作就是内存回收. 处于堆栈上的内存回收及其快速, 处于堆上的内存并不是及时回收的, 此时其对应的内存依然会被标记为使用状态.
- 垃圾回收主要是指堆上的内存分配和回收, Unity 中会定时对堆内存进行 GC 操作.
2. 堆栈内存分配和回收机制
堆栈上的内存分配和回收十分快捷简单, 因为堆栈上只会存储短暂的或者较小的变量. 内存分配和回收都会以一种顺序和大小可控制的形式进行.
堆栈的运行方式就像 stack: 其本质只是一个数据的集合, 数据的进出都以一种固定的方式运行. 正是这种简洁性和固定性使得堆栈的操作十分快捷. 当数据被存储在堆栈上的时候, 只需要简单地在其后进行扩展. 当数据失效的时候, 只需要将其从堆栈上移除.
3. 堆内存分配和回收机制
堆内存上的内存分配和存储相对而言更加复杂, 主要是堆内存上可以存储短期较小的数据, 也可以存储各种类型和大小的数据. 其上的内存分配和回收顺序并不可控, 可能会要求分配不同大小的内存单元来存储数据.
3.1 堆上的变量在存储时的步骤
- 首先, Unity 检测是否有足够的闲置内存单元用来存储数据, 如果有, 则分配对应大小的内存单元
- 如果没有足够的存储单元, Unity 会触发垃圾回收来释放不再被使用的堆内存. 这步操作是一步缓慢的操作, 如果垃圾回收后有足够大小的内存单元, 则进行内存分配.
- 如果垃圾回收后并没有足够的内存单元, 则 Unity 会扩展堆内存的大小, 这步操作会很缓慢, 然后分配对应大小的内存单元给变量.
- 堆内存的分配有可能会变得十分缓慢, 特别是在需要垃圾回收和堆内存需要扩展的情况下, 通常需要减少这样的操作次数.
3.2 垃圾回收时的操作
当堆内存上一个变量不再处于激活状态的时候, 其所占用的内存并不会立刻被回收, 不再使用的内存只会在 GC 的时候才会被回收
每次运行 GC 的时候, 主要进行下面的操作:
- GC 会检查堆内存上的每个存储变量
- 对每个变量会检测其引用是否处于激活状态
- 如果变量的引用不再处于激活状态, 则会被标记为可回收
- 被标记的变量会被移除, 其所占有的内存会被回收到堆内存上
GC 操作是一个极其耗费的操作, 堆内存上的变量或者引用越多则其运行的操作会更多, 耗费的时间越长.
3.3 何时会触发GC
- 在堆内存上进行内存分配操作而内存不够的时候都会触发垃圾回收来利用闲置的内存
- GC 会自动的触发, 不同平台运行频率不一样
- GC 可以被强制执行.
特别是在堆内存上进行内存分配时内存单元不足够的时候, GC会被频繁触发, 这就意味着频繁在堆内存上进行内存分配和回收会触发频繁的 GC 操作
3.4 GC引起的性能问题
主要表现为:
- 帧率下降 —— 在性能瓶颈时期触发GC
- 性能时好时坏
- 断断续续的出现卡顿 —— 每次GC的到来, 会导致系统停止所有进程, 并造成大量的CPU 开销, 进而降低游戏运行的流畅度.
如何降低降低GC带来的影响:
- 减少GC运行所需的时间 —— 减少游戏中的堆内存分配和对象引用数目
- 减少GC频率 —— 降低对内存的分配和释放频率
- 在非性能瓶颈时期主动触发GC —— 尝试测算GC和堆空间扩张的时间来使其在可预测, 适宜的时间发生
3.5 如何减少垃圾的产生
- 使用缓存&不要在频繁调用的方法中分配堆内存. 如果在需要频繁调用的方法中进行了堆内存分配, 并且最后舍弃了这个新建的变量, 这将会产生不必要的垃圾, 应该考虑在方法外创建对该变量的引用, 然后重复利用它. 对于无法缓存的对象, 应该尽量降低方法的执行次数, 仅在需要时才去执行它.
- 使用清空集合替代新建集合