CLR(Common Language Runtime):CLR的核心功能包括内存管理,程序集加载,类型安全,异常处理和线程同步,而且还负责对代码实施严格的类型安全检查,保证代码的准确性,这些功能都可以提供给面向CLR的所有语言
CLR并不关心是使用何种语言进行编程开发,只要编译器是面向CLR而进行编译的即可,这个中间的结果,就是IL(Intermediate Language), 最终面向CLR编译得到的结果是:IL语句以及托管数据(元数据)组成的托管模块

托管就是.net framework 负责帮你管理内存及资源释放,不需要自己控制。
对于引用类型,栈上保存着一个地址而已,当栈释放后, 即使对象已经没有用了,但堆上分配的内存还在,只能等GC收集时才能真正释放。但对于值类型,GC会自动释放他们占用的内存,不需要GC来回收释放
Object.Finalize()方法。默认情况下,方法是空的,对于非托管对象,需要在此方法中编写回收非托管资源的代码,以便垃圾回收器正确回收资源托管代码和非托管代码
托管代码:由公共语言运行库环境(而不是直接由操作系统)执行的代码。托管代码应用程序可以获得公共语言运行库服务,例如自动垃圾回收、运行库类型检查和安全支持等。
非托管代码:在公共语言运行库环境的外部,由操作系统直接执行的代码。非托管代码必须提供自己的垃圾回收、类型检查、安全支持等服务。
Unity自动内存管理机制
利用profiler window 来检测堆内存分配
在CPU usage分析窗口中,我们可以检测任何一帧cpu的内存分配情况。其中一个选项是GC Alloc,通过分析其来定位是什么函数造成大量的堆内存分配操作
当满足以下条件之一时CLR将发生垃圾回收:
GC.Collect 方法。AppDomain)GC是一种分代式垃圾回收器,使用引用计数算法,该算法只关心引用类型变量
根对象:相当于是当前存活对象的合集,GC去搜索能被该集合直接或者是间接指向的对象
下图是回收之前的托管堆模型,根直接引用了对象A,C,D,F。标记对象D时,垃圾回收器发现这个对象含有一个引用对象H的字段,所以H也会被标记,整个过程一直持续到所有根检查完毕。NextObjPtr对象始终保持指向最后一个对象放入托管堆的地址
如果GC发现一个对象已经在图中就会换一个路径继续遍历。这样做有两个目的:一是提高性能,二是避免无限循环。


值得注意的是,将引用赋值为null并不意味着强制GC立即启动并把对象从堆上移除,唯一完成的事情是显式取消了引用和之前 引用所指向对象之间的连接
代的分代式垃圾回收器,而代就是一种为了降低GC对性能影响的机制,垃圾回收有两个基本原理:
托管堆中的每个对象都可以被分为0、1、2三个代,表示他们经历了几次GC仍没有被回收

第 0 代满的时候触发GC,GC后第 0 代对象不包括任何对象,并且第一代对象也已经被压缩整理到连续的地址空间中![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z0mNfsCF-1668434575983)(浅析GC-垃圾回收/image-20221114200703105.png)]](https://1000bd.com/contentImg/2024/04/23/4404fac0119f2498.png)
超过第 0 代预算时再次触发GC,假如第 1 代占用内存远少于预算,GC将只检查第 0 代对象,即便此时原来的第 1 代对象中也出现了垃圾对象

大多数类型只要分配了内存就能够正常工作,但有的类型除了内存还需要本机资源,比如说常用的FileStream,便需要打开一个文件(本机资源)并保存文件句柄,或者是数据库连接信息,那么我们就需要显式释放非托管对象,因为GC仅能跟踪托管堆上的内存资源
Finalize方法(C#中是析构函数),允许对象在判定为垃圾之后,在对象内存在回收之前执行一些代码。当一个对象被判定不可达后,对象将终结它自己,并释放包装着的本机资源,之后,GC再从托管堆中回收对象。
Finalize虽然看似手动清除非托管资源,其实还是由垃圾回收器维护,它的最大作用是确保非托管资源一定被释放
Finalize方法的执行时间无法控制,所以原则上并不提倡使用终结器机制
为了更快更具操作性进行释放,而非让垃圾回收器(即不可预知)来进行,可以使用Dispose,即实现IDispose接口
结构和类类型都可以实现IDispose(与重写Finalize不同,Finalize只适用于类类型),因为不是垃圾回收器来调用Dispose方法,而是对象本身释放非托管资源,这也意味着如果没有调用Dispose()方法,非托管资源永远得不到释放
同样的,Dispose方法也不会将托管对象从托管堆中删除,我们要记住在正常情况下,只有在GC之后,托管堆中的内存才能得以释放。习惯用法是将Dispose方法放入try finally的finally块中,以确保代码的顺利执行
主要有三种方法降低影响:
分别对应着三个策略:
对游戏进行重构,减少堆内存的分配和引用的分配。更少的变量和引用会减少GC操作中的检测个数从而提高GC的运行效率。
降低堆内存分配和回收的频率,尤其是在关键时刻。也就是说更少的事件触发GC操作,同时也降低堆内存的碎片化。
我们可以试着按照可预测的顺序执行。当然这样操作的难度极大
如果在代码中反复调用某些造成堆内存分配的函数但是其返回结果并没有使用,这就会造成不必要的内存垃圾,我们可以缓存这些变量来重复利用
// 下面的代码每次调用的时候就会新分配一个数组,造成堆内存分配
void OnTriggerEnter(Collider other)
{
Renderer[] allRenderers = FindObjectsOfType();
ExampleFunction(allRenderers);
}
// --------修改为---------
private Renderer[] allRenderers;
void Start()
{
allRenderers = FindObjectsOfType();
}
void OnTriggerEnter(Collider other)
{
ExampleFunction(allRenderers);
}
如果是链表等数据结构,记得需要先调用 Clear()函数
同时也要避免在 Update等函数中反复进行堆内存分配
如果游戏有大量的对象需要产生和销毁依然会造成GC。对象池技术可以通过重复使用对象来降低堆内存的分配和回收频率。对象池在游戏中广泛的使用,特别是在游戏中需要频繁的创建和销毁相同的游戏对象的时候,例如枪的子弹
更进一步,可以不将整个游戏物体 SetActive(false)而是只取消掉他的关键组件
在c#中,字符串是引用类型变量而不是值类型变量。
c#中的字符串在创建后是不可变更的。每次在对字符串进行操作的时候(例如运“加”操作),会新建一个字符串用来存储新的字符串,使得旧的字符串被废弃,这样就会造成内存垃圾
在Text文本组件中,我们可以分离常量字符串和需要修改的字符串(尤其是表示UI数字的Text),去除 + 操作符,实施创建的字符串还可以使用 StringBuilder
// 下面的代码中对于每个迭代器都会产生一个新的数组
void ExampleFunction()
{
for(int i=0; i < myMesh.normals.Length;i++)
{
Vector3 normal = myMesh.normals[i];
}
}
// ----修改为---------
void ExampleFunction()
{
Vector3[] meshNormals = myMesh.normals;
for(int i=0; i < meshNormals.Length;i++)
{
Vector3 normal = meshNormals[i];
}
}
调用 StartCoroutine()会产生少量的内存垃圾,因为unity会生成实体来管理协程。所以在游戏的关键时刻应该限制该函数的调用
yield在协程中不会产生堆内存分配,但是如果yield带有参数返回,则会造成不必要的内存垃圾,比如应该将 yield return 0 改为 yield return null,否则会引发装箱
也尽量少返回一个新创建的变量
while(!isComplete)
{
yield return new WaitForSeconds(1f);
}
// 我们可以采用缓存来避免这样的内存垃圾产生:
WaitForSeconds delay = new WaiForSeconds(1f);
while(!isComplete)
{
yield return delay;
}