Version: Unity 6.0 (6000.0)
语言 : 中文
Unity 中的内存
Managed memory introduction

托管内存

Unity 的托管内存系统是基于 Mono 或 IL2CPP 虚拟机 (VM) 的 C# 脚本环境。托管内存系统的优势在于可以管理内存释放,因此无需通过代码手动请求释放内存。

Unity 的托管内存系统使用垃圾收集器托管堆,在脚本不再包含对内存分配的任何引用时自动释放这些分配。这有助于防止内存泄漏。内存泄漏发生在分配了内存但对内存的引用丢失时,之后因为需要引用内存才能释放内存,所以永远不会释放该内存。

此内存管理系统还会保护内存访问,这意味着无法访问已释放的内存或代码从未有效访问的内存。但是,此内存管理过程会影响运行时性能,因为分配托管内存对 CPU 而言是一项耗时的工作。垃圾收集也可能使 CPU 在垃圾收集完成之前停止执行其他工作。

值和引用类型

调用方法时,脚本后端会将其参数的值复制到为该特定调用保留的内存区域(位于称为调用堆栈的数据结构中)。脚本后端可以快速复制占用几个字节的数据类型。但是,对象、字符串和数组通常要大得多,脚本后端无法定期高效复制这些类型的数据。

托管代码中的所有非 null 引用类型对象和所有装箱值类型对象必须分配在托管堆上。

熟悉值和引用类型非常重要,这样才能有效管理代码。有关更多信息,请参阅 Microsoft 有关值类型引用类型的文档。

自动内存管理

创建对象时,Unity 会从称为的中央池中分配存储对象所需的内存,这是 Unity 项目的选定脚本运行时(Mono 或__ IL2CPP__种由 Unity 开发的脚本后端,可在为某些平台构建项目时替代 Mono。更多信息
See in Glossary
)自动管理的一段内存。当此对象不再使用时,先前占用的内存可被回收并用于其他对象。

Unity 的脚本后端使用垃圾收集器来自动管理应用程序的内存,因此无需使用显式方法调用来分配和释放这些内存块。相比显式分配/释放,自动内存管理减少了所需的编码工作量,并且降低了内存泄漏的可能性。

托管堆概览

托管堆是由 Unity 项目的选定脚本运行时(Mono 或 IL2CPP)自动管理的一段内存。

内存量。图中标记 A 表示部分可用内存。
内存量。图中标记 A 表示部分可用内存。

在上图中,蓝色框表示 Unity 分配给托管堆的内存量。白色框表示 Unity 存储在托管堆内存空间中的数据值。当需要其他数据值时,Unity 会从托管堆中为它们分配可用空间(注释 A)。

内存碎片化和堆扩展

内存量,已释放的一些对象由灰色虚线表示。
内存量,已释放的一些对象由灰色虚线表示。

上图为内存碎片化示例。Unity 释放对象时,对象占用的内存也将被释放。但是,释放的空间不会整合成为整个“可用内存”池的一部分。

位于释放对象两侧的对象可能仍在使用中。因此,释放的空间是其他内存段之间的“空隙”。Unity 只能使用此空隙来存储与释放的对象大小相同或更小的数据。

这种情况称为内存碎片化。当堆中有大量可用内存,但仅在对象之间的“空隙”中可用时,就会发生这种情况。这意味着,即使有足够的空间总量用于大型内存分配,托管堆也无法找到足够大的单个连续内存块并将其指定用于分配。

注释为 A 的对象是需要添加到堆中的新对象。注释为 B 的项是已释放的对象占用的内存空间,加上可用的未保留内存。尽管有足够的可用空间总量,但由于没有足够的连续空间,所以注释为 A 的新对象的内存无法放入堆中,垃圾收集器必须运行。
注释为 A 的对象是需要添加到堆中的新对象。注释为 B 的项是已释放的对象占用的内存空间,加上可用的未保留内存。尽管有足够的可用空间总量,但由于没有足够的连续空间,所以注释为 A 的新对象的内存无法放入堆中,垃圾收集器必须运行。

如果分配了大型对象,却没有足够的连续可用空间来容纳该对象(如上所示),Unity 内存管理器将执行两个操作:

  • 首先,如果垃圾收集器尚未运行,则运行垃圾收集器。此工具会尝试释放足够的空间来满足分配请求。
  • 如果在垃圾收集器运行后,仍然没有足够的连续空间来满足请求的内存量,则必须扩展堆。具体的堆扩展量取决于平台;但是,在大多数平台上,当堆扩展时,其扩展量是上次扩展量的两倍。

托管堆扩展注意事项

意外扩展堆可能导致问题。Unity 的垃圾收集策略往往会更频繁地使内存碎片化。应注意以下事项:

  • Unity 在常规扩展时不会释放分配给托管堆的内存;相反,即使堆的大部分为空,它也会保留扩展后的堆。这是为了防止再次出现大型分配时需要重新扩展堆。
  • 在大多数平台上,Unity 最终会将托管堆的空置部分使用的内存释放回操作系统。发生这种情况的时间间隔无法保证,也不会保持一致。
  • 垃圾收集器不会清除本机内存对象或其他本机分配。Resources.UnloadUnusedAssets 对任何不再有任何引用的本机对象执行此操作。它还会触发一个 GC.Collect 以确保这些引用的状态是最新的。请记住:
    • UnloadUnusedAssets 不会自动触发,只会手动触发和在场景变化时触发。如果要提前释放该内存(例如,对于低 RAM 平台上的全屏渲染纹理,可能必须执行此操作),则应在对象上调用 Destroy,以最佳方式利用内存。
    • 存在托管内存泄漏的风险。如果保留了对已清理对象的引用,则可能泄漏托管对象引用的本机对象。静态字段和事件是此类内存泄漏的常见来源。有关如何使用 Memory Profiler 分析这些问题的更多信息,请参阅 Memory Profiler 包文档中的托管 shell 对象
    • 调用 UnloadUnusedAssetsGC.Collect 会触发可能希望在游戏过程中避免的 CPU 密集型操作。
Unity 中的内存
Managed memory introduction