青岛工程建设管理信息网站下载,京东网站建设吗,wordpress升级文章编辑器,wordpress切换背景Unity垃圾回收原理 
参考文章#xff1a;垃圾回收 (计算机科学) - 维基百科#xff0c;自由的百科全书 (wikipedia.org) 
在计算机科学中#xff0c;垃圾回收#xff08;英语#xff1a;Garbage Collection#xff0c;缩写为GC#xff09;是指一种自动的存储器管理机制。…Unity垃圾回收原理 
参考文章垃圾回收 (计算机科学) - 维基百科自由的百科全书 (wikipedia.org) 
在计算机科学中垃圾回收英语Garbage Collection缩写为GC是指一种自动的存储器管理机制。当某个程序占用的一部分内存空间不再被这个程序访问时这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。垃圾回收器可以减轻程序员的负担也减少程序中的错误。 
原理 
垃圾回收器有两个基本的原理 
考虑某个对象在未来的程序执行中将不会被访问。回收这些对象所占用的存储器。 
分类 
收集器实现 
引用计数收集器 
主条目引用计数 
最早的也是最简单的垃圾回收实现方法这种方法为占用物理空间的对象附加一个计数器当有其他对象引用这个对象时计数器加一反之引用解除时减一。这种算法会定期检查尚未被回收的对象的计数器为零的话则回收其所占物理空间因为此时的对象已经无法访问。这种方法无法回收循环引用的存储对象。 
跟踪收集器 
主条目追踪垃圾回收 
近现代的垃圾回收实现方法这种算法会定期遍历它管理的内存空间从若干根储存对象开始查找与之相关的存储对象然后标记其余的没有关联的存储对象最后回收这些没有关联的存储对象占用的内存空间。 
回收算法 
基于其标记和回收行为又分为若干细致方法。 
标记清除 
先暂停整个程序的全部运行线程让回收线程以单线程进行扫描标记并进行直接清除回收然后回收完成后恢复运行线程。这样会产生大量的空闲空间碎片和使大容量对象不容易获得连续的内存空间而造成空间浪费。 
标记压缩 
和“标记清除”相似不同的是回收期间同时会将保留的存储对象搬运汇集到连续的内存空间从而整合空闲空间避免内存碎片化。 
复制 
需要程序将所拥有的内存空间分成两个部分。程序运行所需的存储对象先存储在其中一个分区定义为“分区0”。同样暂停整个程序的全部运行线程进行标记后回收期间将保留的存储对象搬运汇集到另一个分区定义为“分区1”完成回收程序在本次回收后将接下来产生的存储对象会存储到“分区1”。在下一次回收时两个分区的角色对调。[3] 
这种方式非常简单但是因为只有一个“半空间”semi-space被用于分配对象内存使用相较于其他算法是其两倍。这种技术也叫做“停止并复制”。Cheney算法是改进的半空间分配器。 
增量回收器 
需要程序将所拥有的内存空间分成若干分区。程序运行所需的存储对象会分布在这些分区中每次只对其中一个分区进行回收操作从而避免暂停所有正在运行的线程来进行回收允许部分线程在不影响回收行为下保持运行并且降低回收时间增加程序响应速度。 
分代 
由于“复制”算法对于存活时间长大容量的储存对象需要耗费更多的移动时间和存在储存对象的存活时间的差异。需要程序将所拥有的内存空间分成若干分区并标记为年轻代空间和年老代空间。程序运行所需的存储对象会先存放在年轻代分区年轻代分区会较为频密进行较为激进垃圾回收行为每次回收完成幸存的存储对象内的寿命计数器加一。当年轻代分区存储对象的寿命计数器达到一定阈值或存储对象的占用空间超过一定阈值时则被移动到年老代空间年老代空间会较少运行垃圾回收行为。一般情况下还有永久代的空间用于涉及程序整个运行生命周期的对象存储例如运行代码、数据常量等该空间通常不进行垃圾回收的操作。 通过分代存活在局限域小容量寿命短的存储对象会被快速回收存活在全局域大容量寿命长的存储对象就较少被回收行为处理干扰。 
现今的GC如Java和.NET使用分代收集generation collection依照对象存活时间的长短使用不同的垃圾收集算法以达到最好的收集性能。 原文链接Unity GC 学习总结 - 知乎 (zhihu.com) 
什么是GC 
总所周知内存是程序运行时所需要的重要资源在程序运行时往往需要内存来临时存储各种数据但是操作系统提供给进程的堆内存注意是堆内存栈上的内存会随函数调用自动被回收下文提及的都是指堆内存是有限的所以我们需要对这有限的资源进行管理 
在代码中我们会反复地申请内存来完成各种计算等到确认内存不需要使用时我们就会归还这部分内存从而可以将其用于其他地方。GC所做的事情就是自动确定那些不需要的内存或者说 Garbage 然后将其归还。这样开发者就无需关心内存的管理。 
GC的实现 
实现GC的策略有很多种其中最常见一种就是 Tracing garbage collection或者叫 Mark-Sweep这种算法会通过一个 root Object遍历这个该对象引用的变量并且标记递归这个过程这样就确定了所有reachable的对象剩下的对象即视为garbage。 
另一种常见的策略还有引用计数Reference counting它是通过为每个对象维护一个引用计数这代表当前对该对象的引用数目当引用为0即代表该对象为 Garage。引用技术有如下缺点 
循环引用问题保存计数带来的空间开销修改引用数目带来的速度开销以及原子性要求非实时一个引用的变化可能递归得导致一系列引用修改内存释放) 
有很多算法可以一定程度解决上述问题顺便一提C使用的智能指针即是基于引用计数实现的COM对象也使用了引用计数来管理。 
GC的优缺点 
优点 
如上文提及的可以将程序从对内存的维护中解放出来专心于代码逻辑。不会发生因为内存管理不当而导致的问题例如 
内存泄漏访问已经释放的指针反复释放指针 
缺点 
那么代价是什么呢享受 GC 带来的便利意味你必须承受 GC 开销对性能的影响眼睁睁地看着它费老大劲去处理一个你一眼看出来的 Garbage 。比如 
Unity 中的GC 
Unity的脚本后端是基于Mono的实现当然现在多了个IL2CPP不过也是类似的GC实现)而Mono使用的GC是所谓的Boehm–Demers–Weiser garbage collector。是Mark-Sweep 的实现它会在需要进行GC时占用主线程进行遍历-标记-垃圾回收的过程然后在归还主线程控制权。这会导致帧数的突然下降产生卡顿不过因为该实现是非压缩式的所以卡顿现象相对较轻但是对内存利用率进一步下降了会有内存碎片的问题。。囧。所以我们需要慎重地处理对象的创建内存请求还有释放使用GC管理内存是没有主动释放内存的接口的但是我们可以通过消除对某个对象的引用来做到这一点。此外Unity的代码分为两部分托管与非托管GC影响的只有托管部分的代码使用的堆内存。而且这个托管堆占用的地址空间不会返还给操作系统非托管内存需要手动维护 
GC的优化 
上文讲到了GC对性能影响的原因占用主线程进行大量工作而优化GC即是减小GC占用主线程时花费的CPU时间所以优化GC优化的是CPU时间而非内存事实上常见的优化GC的手段之一就是占用内存 
排查热点 
优化的第一步就是确定性能热点我们可以使用 Unity 自带的 Profiler 中 CPU Usage里的Garbage Collector来确定或者粗暴一点使用 GarbageCollector.GCMode 这一接口来关掉GC然后观察 Profiler 中 Memory里的 Total GC Allocated 来确定。不过该接口无法用于编辑器下。 
常见热点与优化方式 
GC优化的核心在于消除垃圾减小GC运行时间。GC的热点一般都是写了一些会产生大量垃圾的代码。 
1.字符串 
using UnityEngine;
using UnityEngine.UI;
using System.Collections;public class ExampleScript : MonoBehaviour {public Text scoreBoard;public int score;void Update() {string scoreText  Score:   score.ToString();scoreBoard.text  scoreText;}
} 
上述代码中拼接字符串会导致一些额外的中间对象产生所以会大量创建临时的变量可以通过使用StringBuilder来优化。此外还在Update中每帧调用进一步恶化了问题创建了更多的临时变量。可以通过将变量改为非局部变量来解决(这也就是上面讲的占用内存优化GC)上述代码即可以优化成 
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Text;public class ExampleScript : MonoBehaviour
{public Text scoreBoard;public StringBuilder scoreText;public int score;public int oldScore;void Update(){if (score ! oldScore){scoreText.Clear();scoreText.Append(Score: );scoreText.Append(score.ToString());scoreBoard.text  scoreText.ToString();oldScore  score;}}
} StringBuilder    
[C#] StringBuilder简介及使用方法_c# stringbuilder length方法-CSDN博客 
C#中StringBuilder弥补了string在赋值时开辟新空间不足之处。 
StringBuilder类型变量会初始化一段长度供后续对该变量进行增加。当然也可以手动定义其长度 StringBuilder builder  new StringBuilder(10); 其缺点是需要较为精确估算出StringBuilder类型变量的长度否则若在使用中实际builder长度超出了定义的长度会自动开辟一段新的StringBuilder空间并将原先的数据赋值给新的空间旧的地址就变成了垃圾。 
StringBuilder builder  new StringBuilder(10);
for (int i  1; i  10; i) 
{builder.Append(i); // 0123456789
} 
整个操作都是在一处内存地址提高了内存利用率。在Unity实际开发时也有很大的用处 
实例方法 
单词反转 Hello world  world Hello 
private static StringBuilder t3(string str)
{string[] str2  str.Split( );StringBuilder builder  new StringBuilder(str.Length);for (int i  str2.Length - 1; i  0; i--){builder.Append(str2[i]);if (i ! 0)builder.Append( );}return builder;
}// 调用
Console.WriteLine(t3(Hello world)); // world Hello 
2.闭包 
闭包的使用也需要慎重因为闭包除了函数指针还会将捕获的变量一起包装起来创建到堆上相当于 new 了个对象性能敏感部分代码还是要慎重使用。可以通过将匿名函数改为成员函数捕获变量改为成员变量一定程度上缓解不过还是会有影响。 
3.装箱 
还有要小心装箱这也会隐式地导致对象的创建。从而产生意想不到的垃圾。用枚举值当字典的key的时各种字典操作会调用 Object.getHashCode 获取哈希值 该方法会导致装箱。Unity5.5版本以前 foreach 会导致装箱这之后的版本修复了这个问题但是 foreach相比起直接使用下标遍历还是要慢因为有一些额外的方法调用不过这就和GC没啥关系了。 
4.返回数组的Unity API 
应该是为了防止意外修改内部值Unity API返回数组对象时返回的是一份拷贝。类似下面的代码 
for(int i  0; i  mesh.vertices.Length; i){float x, y, z;x  mesh.vertices[i].x;y  mesh.vertices[i].y;z  mesh.vertices[i].z;// ...DoSomething(x, y, z);   } 
会导致4次数组拷贝可以通过cache返回的数组返回引用解决来解决。 
5.空数组 
空数组长度为0的数组的创建事实上也会导致堆内存的分配。所以应该将其提前创建出来并复用。 
上述问题的原因都是类似的即大量地创建了短暂使用的对象垃圾基本上都可以通过将会反复使用的对象创建为非局部变量来解决或者更进一步使用所谓对象池的技术基本原理是一样的。有些地方就只能通过避免会造成垃圾产生的接口来解决。总之优化GC核心在于消灭垃圾。 
特别的技巧 
1.关闭GC 
可以把需要的内存先全部创建完然后关掉GC不过感觉这种方式应用场景太有限。 
2.主动定时GC 
游戏的卡顿来自与不稳定的帧数变化稳定的低帧数和不稳定的高帧数前者可以带来更平滑的体验所以可以按一定间隔主动地调用 System.GC.Collect 进行GC这样就不会有剧烈的毛刺产生当然这个间隔不能太小否则就和不主动调用区别不大但也不能太小否则会对帧数造成明显影响具体数值的确定还是很难的。 
3.主动扩大托管堆 
Mono的GC会尽量避免内存的扩展所以说它对判断 需要进行GC 了的阈值比较低可能已分配内存达到当前GC管理内存的70%~80%就会进行GC了如果GC的持有内存足够大的话就会减少GC的触发可以通过类似下面的代码 
using UnityEngine;
using System.Collections;public class ExampleScript : MonoBehaviour {void Start() {var tmp  new System.Object[1024];// make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocksfor (int i  0; i  1024; i)tmp[i]  new byte[1024];// release referencetmp  null;}
} 
来强行扩大GC管理内存的大小。不过实际开发中还有贴图之类的内存大户留给GC的可以内存实在不多盲目请求过大的内存可能会被操作系统无情干掉要慎重。而且因为托管堆占用的地址空间并不会归还所以请求太大的托管堆会导致内存的浪费。。这种做法算是空间换时间。 
增量式GCincremental garbage collection 
上文提到的 Unity GC实现是非分代式的也就是是说要么不做要做就一次性作完。unity 在 2018 的版本推出了所谓增量式GC的功能还是基于 Boehm–Demers–Weiser garbage collector 的实现但是不再是非分代式的这能带来特别的技巧♂中第二点同样的好处即均衡负载到多帧消除毛刺。可以缓解卡顿。因为GC的执行分配到每帧了所以单帧GC的执行时间会受到垂直同步 还有 unity 的 Application.targetFrameRate 的影响。 
增量式GC目前还是抢先体验版本因为它事实上还是存在一些问题它的基本实现原理还是标记-清扫但是在两次增量式GC之间对象的引用可能会发生变化导致前一次GC的标记失效需要重新进行遍历标记最糟的情况会退化为普通的非分代GC(其实更糟因为前面的工作全白费了。比如这样的代码 
void Update(){if (Time.frameCount % 2  0){s  A;}else{s  B;}} 
A 和 B 在垃圾与不是垃圾之间反复横跳字符串常量的引用可能不太一样但这里是为了表达对象引用情况反复变化的意思。而且增量式GC还需要额外的开销来确定对象的引用是否变化这开销也不可忽视实际项目看对毛刺的容忍程度来确定要不要使用增量式GC而且要好好地做Profiler很容易一不小心就负优化了。 贝姆垃圾收集器  原文链接贝姆垃圾收集器 - 维基百科自由的百科全书 (wikipedia.org) 
Boehm-Demers-Weiser garbage collector也就是著名的Boehm GC是计算机应用在C/C语言上的一个保守的垃圾回收器 原文链接Unity 垃圾回收GC的原理 - 知乎 (zhihu.com) 
1、BoehmGC中的内存分配 
作为一个重量级的基础组件库BoehmGC的使用方法非常简单只需要把系统函数malloc替换为GC_malloc即可之后你就完全不用管何时free的问题。在小型项目里你甚至可以直接 
#define malloc(n) GC_malloc(n) 
然后再不用管freeBoehmGC自会帮你打理好一切。 
既然全盘接管了内存分配那就必须做到以下两点才能称得上是合格的分配器 
1. 分配的效率要高 
2. 尽量避免内存浪费避免碎片化等 
那BoehmGC是怎么做的呢 
2、BoehmGC的内存分配架构 
在整个内存分配链的最底部BoehmGC通过平台相关接口来向操作系统申请内存可能是malloc, sbrk, 或者mmap等。为了提高效率会根据配置每次批量申请4K的倍数大小除了用户能使用的内存之外还有BoehmGC内部维护的数据结构(通过GC_scratch_alloc分配)。 
分配器的核心是一个分级的结构BoehmGC把每次申请根据内存大小归类成小内存对象(Small Object)和大内存对象(Large Object)这点和STL的分配器也比较相似。归类的依据具体来说就是 
1不超过PageSize/2也就是2048字节的对象为小内存对象 
2大于PageSize/2的对象为大内存对象 
//heap block定义了一个页大小为4K的倍数
struct hblk {char hb_body[HBLKSIZE];
}; 
对于Large Object向上取整到4K的倍数大小直接以整数个hblk的形式给出。 
而Small Object则会先申请一个hblk出来而后在这块内存上进一步细分为Small Objects形成free-list。 
3、BoehmGC的内存管理策略 
为了尽量减少碎片化和加速分配BoehmGC在设计上就做了一些限制充分体现了“物以类聚”的思想。 
首先GC管理的对象有一个最小的“粒度”即Granule。 
32位上这个值是8字节64位则是16字节。 
在64位环境下即使用户申请的内存是10个字节也会被向上调整到16字节。 
一个在用的hblk如果不是属于一个large object那就是容纳了若干个等大小的small object。 
对于有一定内存分配器实现经验的开发者来说以上两点应该都比较熟悉了不过BoehmGC把这种“物以类聚”的设计贯彻落实得更加彻底。 
对于大内存对象(large object)按照对应的hblk数把他们归类到若干个freelist中。具体的做法可以参考GC_hblk_fl_from_blocks和GC_allochblk_nth。 
当大内存对象被垃圾回收的时候会尝试把相邻的hblk合并减少内存碎片。 
对于小内存对象的大小分档也不是完全按照Granule的等差数列来决定。有些临近的大小会被优化合并掉比如系统当前有很多1024字节的闲置块但申请1008字节的小内存对象仍然可能miss。此时用1024字节的块可能是更好的选择适当的合并临近的block size可以优化内存分配效率。这块的做法可以参考GC_init_size_map和GC_extend_size_map。 
三、和lua GC有什么区别 
不知道有没有同学想过lua中的GC用到是什么算法。 
Lua 5.3 的垃圾回收机制采用的是标记-清除算法mark-and-sweep它会对所有经过 Lua 管理的内存进行垃圾回收但不会回收非 Lua 管理的内存例如使用 malloc 或者 new 分配的内存。该算法的优点是实现简单效率高但缺点是可能会产生内存碎片。 
而Boehm GCGarbage Collector是一个通用的垃圾回收库可以用于 C 和 C 语言中的动态分配的内存 
四、和Java 、C# GC的区别 
Java和 C# GC 都是精准式GC,而Boehm GC 是保守式的。 
下面是一个比较官方的回答 
“保守式垃圾回收是一种通过近似方式识别和回收垃圾对象的方式。在保守式垃圾回收中垃圾回收器并不直接访问对象的内部结构和引用关系而是通过扫描内存中的数据块识别出可能是指向对象的指针并将其标记为活动对象。然后垃圾回收器将从活动对象出发递归地遍历和标记其他可达的对象并回收那些未被标记为活动对象的内存。保守式垃圾回收不需要额外的内存开销来维护对象之间的引用关系但可能会存在一定的误判即将某些实际上是垃圾的对象错误地标记为活动对象。” 
用大白话讲就是Boehm GC无法区分指针和非指针这就可能由于误判导致有些已经可以释放的内存无法释放。 
而Java和 C# GC是可以的但是需要付出一定的代价。 Lua GC 
以后补充