Unity 的托管内存系统是基于 Mono 或 IL2CPP 虚拟机 (VM) 的 C# 脚本环境。托管内存系统的优势在于可以管理内存释放,因此无需通过代码手动请求释放内存。
Unity 的托管内存系统使用垃圾收集器和托管堆,在脚本不再包含对内存分配的任何引用时自动释放这些分配。这有助于防止内存泄漏。内存泄漏发生在分配了内存但对内存的引用丢失时,之后因为需要引用内存才能释放内存,所以永远不会释放该内存。
此内存管理系统还会保护内存访问,这意味着无法访问已释放的内存或代码从未有效访问的内存。但是,此内存管理过程会影响运行时性能,因为分配托管内存对 CPU 而言是一项耗时的工作。垃圾收集也可能使 CPU 在垃圾收集完成之前停止执行其他工作。
调用方法时,脚本后端会将其参数的值复制到为该特定调用保留的内存区域(位于称为调用堆栈的数据结构中)。脚本后端可以快速复制占用几个字节的数据类型。但是,对象、字符串和数组通常要大得多,脚本后端无法定期高效复制这些类型的数据。
托管代码中的所有非 null 引用类型对象和所有装箱值类型对象必须分配在托管堆上。
熟悉值和引用类型非常重要,这样才能有效管理代码。有关更多信息,请参阅 Microsoft 有关值类型和引用类型的文档。
创建对象时,Unity 会从称为堆的中央池中分配存储对象所需的内存,这是 Unity 项目的选定脚本运行时(Mono 或__ IL2CPP__种由 Unity 开发的脚本后端,可在为某些平台构建项目时替代 Mono。更多信息
See in Glossary)自动管理的一段内存。当此对象不再使用时,先前占用的内存可被回收并用于其他对象。
Unity 的脚本后端使用垃圾收集器来自动管理应用程序的内存,因此无需使用显式方法调用来分配和释放这些内存块。相比显式分配/释放,自动内存管理减少了所需的编码工作量,并且降低了内存泄漏的可能性。
托管堆是由 Unity 项目的选定脚本运行时(Mono 或 IL2CPP)自动管理的一段内存。
在上图中,蓝色框表示 Unity 分配给托管堆的内存量。白色框表示 Unity 存储在托管堆内存空间中的数据值。当需要其他数据值时,Unity 会从托管堆中为它们分配可用空间(注释 A)。
上图为内存碎片化示例。Unity 释放对象时,对象占用的内存也将被释放。但是,释放的空间不会整合成为整个“可用内存”池的一部分。
位于释放对象两侧的对象可能仍在使用中。因此,释放的空间是其他内存段之间的“空隙”。Unity 只能使用此空隙来存储与释放的对象大小相同或更小的数据。
这种情况称为内存碎片化。当堆中有大量可用内存,但仅在对象之间的“空隙”中可用时,就会发生这种情况。这意味着,即使有足够的空间总量用于大型内存分配,托管堆也无法找到足够大的单个连续内存块并将其指定用于分配。
如果分配了大型对象,却没有足够的连续可用空间来容纳该对象(如上所示),Unity 内存管理器将执行两个操作:
意外扩展堆可能导致问题。Unity 的垃圾收集策略往往会更频繁地使内存碎片化。应注意以下事项:
GC.Collect 以确保这些引用的状态是最新的。请记住:
UnloadUnusedAssets 不会自动触发,只会手动触发和在场景变化时触发。如果要提前释放该内存(例如,对于低 RAM 平台上的全屏渲染纹理,可能必须执行此操作),则应在对象上调用 Destroy,以最佳方式利用内存。UnloadUnusedAssets 或 GC.Collect 会触发可能希望在游戏过程中避免的 CPU 密集型操作。