原文:Memory in Unity - Unity 手册
Unity内存管理
为了确保您的应用程序运行时没有性能问题,了解Unity如何使用和分配内存非常重要。本文档的这一部分解释了Unity中内存是如何工作的,适用于希望了解如何提高应用程序内存性能的读者。
Unity使用三个内存管理层来处理应用程序中的内存:
1. 托管内存:一个受控的内存层,使用托管堆和垃圾收集器来自动分配和分配内存。
2. C#非托管内存:与Unity Collections命名空间和包一起使用的内存管理层。这种内存类型称为“非托管”,因为它不使用垃圾收集器来管理未使用的内存部分。
3. 本地内存:Unity用于运行引擎的C++内存。在大多数情况下,这个内存对Unity用户是不可访问的,但如果您想要微调应用程序性能的某些方面,了解它可能会有用。
托管内存
Mono和IL2CPP的脚本虚拟机(VM)实现了托管内存系统,有时也称为脚本内存系统。这些VM提供了一个受控内存环境,分为以下不同类型:
- 托管堆:VM自动控制的内存部分,使用垃圾收集器(GC)来管理。因此,分配在托管堆上的内存被称为GC分配。分配的任何发生都会在Profiler中记录为GC.Alloc样本。
- 脚本堆栈:当应用程序进入和退出任何代码范围时,堆栈会构建和解开。
- 本地VM内存:包含与Unity的脚本层相关的内存。在大多数情况下,您不需要操作本地VM内存,但了解它包括与代码生成相关的执行代码内存,尤其是关于泛型使用、Reflection使用的类型元数据以及运行VM所需的内存。
由于托管内存系统使用VM,它具有受控环境,自动跟踪分配的引用以管理其生命周期。这意味着您的应用程序不太可能在其他代码尝试访问内存时过早释放内存。这还意味着您对内存泄漏有一些保障,当内存从代码或未使用的内存堆积中不可访问时发生。
在Unity中使用托管内存是管理应用程序内存的最简单方法;但它也有一些缺点。垃圾收集器很方便使用,但在释放和分配内存时的方式不可预测,这可能会导致性能问题,比如当垃圾收集器必须停止释放和分配内存时会发生卡顿。为了解决这种不可预测性,您可以使用C#非托管内存层。
有关托管内存工作原理的更多信息,请参阅托管内存文档。
C#非托管内存
C#非托管内存层允许您访问本地内存层以微调内存分配,同时编写C#代码非常方便。
您可以使用Unity核心API中的Unity.Collections命名空间(包括NativeArray)以及Unity Collections包中的数据结构来访问C#非托管内存。如果您使用Unity的C#作业系统或Burst,您必须使用C#非托管内存。有关更多信息,请参阅作业系统和Burst的文档。
本地内存
Unity引擎的内部C/C++核心具有自己的内存管理系统,称为本地内存。在大多数
情况下,您不能直接访问或修改这种内存类型。
Unity将您项目中的场景、资产、图形API、图形驱动程序、子系统和插件缓冲区以及本地内存内的分配都存储在本地内存中,这意味着您可以通过Unity的C# API间接访问本地内存。这意味着您可以安全且轻松地操作应用程序的数据,而不会失去Unity本地核心中的本地和高性能代码的好处。
大多数情况下,您不需要与Unity的本地内存交互,但每当您使用Profiler时,都可以通过Profiler标记来查看它如何影响应用程序的性能。
原文:Managed memory - Unity 手册
Managed memory
Unity的托管内存系统是基于Mono或IL2CPP虚拟机(VMs)的C#脚本环境。托管内存系统的好处在于它管理内存的释放,因此您不需要通过代码手动请求释放内存。
Unity的托管内存系统使用垃圾回收器和托管堆来自动释放内存分配,当您的脚本不再持有对这些分配的引用时。这有助于防止内存泄漏。内存泄漏发生在分配了内存后,对它的引用丢失,然后永远不会释放内存,因为它需要引用才能释放。
这个内存管理系统还保护内存访问,这意味着您不能访问已被释放的内存,或者对于您的代码来说从未有效的内存。然而,这个内存管理过程会影响运行时性能,因为分配托管内存对CPU来说是耗时的。垃圾回收也可能会在完成之前阻止CPU执行其他工作。
值类型和引用类型
当调用一个方法时,脚本后端会将其参数的值复制到为该特定调用保留的内存区域中,这个数据结构称为调用堆栈。脚本后端可以快速复制占用几个字节的数据类型。然而,对象、字符串和数组通常要大得多,而且脚本后端定期复制这些类型的数据是低效的。
在托管代码中,所有非空引用类型对象和所有装箱的值类型对象都必须分配在托管堆上。
熟悉值类型和引用类型对于有效管理代码很重要。有关更多信息,请参阅Microsoft的有关值类型和引用类型的文档。
自动内存管理
当创建对象时,Unity会从称为堆的中央池中分配所需的内存,这是您的Unity项目选择的脚本运行时(Mono或IL2CPP)自动管理的内存部分。当对象不再使用时,曾经占用的内存可以被回收并用于其他用途。
Unity的脚本后端使用垃圾回收器来自动管理应用程序的内存,因此您不需要使用显式方法调用来分配和释放这些内存块。自动内存管理需要的编码工作较少,并减少了内存泄漏的可能性。
托管堆概述
托管堆是您的Unity项目选择的脚本运行时(Mono或IL2CPP)自动管理的内存部分。
在上面的图表中,蓝色框表示Unity分配给托管堆的一定数量的内存。其中的白色框代表Unity在托管堆的内存空间内存储的数据值。当需要额外的数据值时,Unity会从托管堆(标有A的地方)分配它们的空间。
内存碎片和堆扩展
上图显示了内存碎片的示例。当Unity释放一个对象时,该对象占用的内存会被释放。然而,空闲空间不会成为一个“自由内存”的单一大池。
已释放对象的两侧可能仍在使用。因此,被释放的空间是其他内存段之间的“间隙”。Unity只能使用这个间隙来存储与已释放对象相同或更小尺寸的数据。
这种情况被称为内存碎片化。当堆中有大量可用内存,但只能在对象之间的“间隙”中使用时,就会发生这种情况。这意味着即使总共有足够的空间来分配大块内存,托管堆也找不到足够大的连续内存块来分配给该分配。
(被标注为A的对象是需要添加到堆上的新对象。被标注为B的项目是已释放对象占用的内存空间,以及空闲的未分配内存。尽管总的空闲空间足够,但由于没有足够的连续空间,所以被标注为A的新对象的内存无法适应堆,因此必须运行垃圾收集器。)
如果分配了一个大对象,并且没有足够的连续空闲空间来容纳它,如上所示,Unity内存管理器会执行两个操作:
首先,如果垃圾回收器尚未运行,则运行垃圾回收器。这试图释放足够的空间来满足分配请求。
如果在垃圾回收器运行后,仍然没有足够的连续空间来容纳请求的内存量,则必须扩展堆。堆扩展的具体量取决于平台,但在大多数平台上,当堆扩展时,它会扩展到前一次扩展的两倍。
托管堆扩展注意事项
堆意外扩展可能会引发问题。Unity的垃圾收集策略倾向于更频繁地碎片化内存。您应该注意以下事项:
Unity不会在定期扩展堆时释放分配给托管堆的内存;相反,它会保留扩展的堆,即使其中的大部分部分为空。这是为了防止需要重新扩展堆,如果发生更多的大型分配。
在大多数平台上,Unity最终会将托管堆的空闲部分使用的内存释放回操作系统。这种情况发生的间隔不受保证,不可靠。