一、什么是垃圾回收机制
GC就是Garbage Collector(垃圾收集器),在一定的时间去遍历托管应用程序在堆上动态分配的所有对象,通过它们是否被引用来判断哪些对象还在使用哪些对象已经不需要再使用,不再使用的对象就是我们程序中的垃圾,需要被回收,这就是GC垃圾回收机制的工作原理。垃圾回收机制在工作中也会依赖很多的算法,就比如:引用计数法、标记清除法、分代法、跟搜索法、标记压缩法等等!
重要知识点
托管:自动的垃圾回收
非托管:手动的进行垃圾回收
二、我们为什么要对GC进行优化呢?
1、GC并不能释放所有的资源。它不能自动释放非托管资源。
2、GC并不是实时性的去释放系统里面的垃圾,这会造成系统性能上的瓶颈和不确定性。这就需要我们自己去调用Dispose方法区释放非托管资源。
注意
有析构函数的对象需要垃圾回收器两次才能删除:第一次调用析构函数,没有删除对象:第二次调用才真正删除对象!
三、算法实现
1、引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。这也就是需要回收的对象。引用计数算法是对象记录自己被多少程序引用,引用计数为零的对象将被清除。计数器表示的是有多少程序引用了这个对象(被引用数)。计数器是无符号整数。
优点:
1>可立即回收垃圾
每个对象都知道自己的引用计数,当变为0时可以立即回收,将自己接到空闲链表
2>最大暂停时间短
因为只要程序更新指针时程序就会执行垃圾回收,也就是每次通过执行程序生成垃圾时,这些垃圾都会被回收,内存管理的开销分布于整个应用程序运行期间,无需挂起应用程序的运行来做,因此消减了最大暂停时间(但是增多了垃圾回收的次数)
最大暂停时间,因执行 GC 而暂停执行程序的最长时间。
3>不需要沿指针查找
产生的垃圾立即就连接到了空闲链表,所以不需要查找哪些对象是需要回收的
缺点:
1>计数器值的增减处理频繁
因为每次对象更新都需要对计数器进行增减,特别是被引用次数多的对象。
2>计数器需要占用很多位
计数器的值最大必须要能数完堆中所有对象的引用数。比如我们用的机器是32位,那么极端情况,可能需要让2的32次方个对象同时引用一个对象。这就必须要确保各对象的计数器有32位大小。也就是对于所有对象,必须保留32位的空间。
假如对象只有两个域,那么其计数器就占用了整体的1/3。
3>循环引用无法回收
这个比较好理解,循环引用会让计数器最小值为1,不会变为0。
2、标记清除法
标记清除法一般都会分为两步,第一步就是标记,第二代就是清除。
标记的话我们可以采用特定的算法(如引用计数法、可达性分析算法等)标记处内存中哪些对象可以回收,哪些对象还要继续使用。
清除的话我们直接根据标记去回收带有标记的对象即可。
缺点:
1、标记与清除效率低
2、清除之后内存会产生大量碎片。
3、分代法
我们一般游戏运行起来都要占用很大的内存,如果我们直接对这样的内存区域进行GC的操作成本会很高,,所以我们需要根据统计学的一些知识来完善我们的算法。
我们可以将对象按照生命周期来分成新的、老的,对新、老区域采用不同的回收策略和算法,加强对新区域的回收力度,争取在较短的时间内对较小的内存区域内把一些不再使用的对象回收掉。
分代算法的假设有很多的前提条件,就比如:
1、新创建的对象生命周期比较短,较老的对象生命周期长
2、对部分内存进行垃圾回收要比全部内存回收的效率要高。
3、新创建的对象之间关联程度比较高,堆分配的对象是连续的,程序把堆分为3个代龄区域:0、1、2代。
我们每一代都有自己的存储上限,当我们的第0代的内存达到阀值后,则触发0代GC开始进行回收,0代里面剩余的对象进入1代,后面的1代也是同样的步骤。
四、GC注意事项:
1、只管理内存,非托管资源,如文件句柄,GDI资源,数据库连接等还需要用户去管理
2、循环引用,网状结构等的实现会变得简单。GC的标志也压缩算法能有效的检测这些关系,并将不再被引用的网状结构整体删除。
3、GC通过从程序的根对象开始遍历来检测一个对象是否可被其他对象访问,而不是用类似于COM中的引用计数方法。
4、GC在一个独立的线程中运行来删除不再被引用的内存
5、GC每次运行时会压缩托管堆
6、你必须对非托管资源的释放负责。可以通过在类型中定义Finalizer来保证资源得到释放。
7、对象的Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间。注意并非和C++中一样在对象超出声明周期时立即执行析构函数
8、Finalizer的使用有性能上的代价。需要Finalization的对象不会立即被清除,而需要先执行Finalizer.Finalizer不是在GC执行的线程被调用。GC把每一个需要执行Finalizer的对象放到一个队列中去,然后启动另一个线程来执行所有这些Finalizer.而GC线程继续去删除其他待回收的对象。在下一个GC周期,这些执行完Finalizer的对象的内存才会被回收。
9、.NET GC使用"代"(generations)的概念来优化性能。代帮助GC更迅速的识别那些最可能成为垃圾的对象。在上次执行完垃圾回收后新创建的对象为第0代对象。经历了一次GC周期的对象为第1代对象。经历了两次或更多的GC周期的对象为第2代对象。代的作用是为了区分局部变量和需要在应用程序生存周期中一直存活的对象。大部分第0代对象是局部变量。成员变量和全局变量很快变成第1代对象并最终成为第2代对象。
10、GC对不同代的对象执行不同的检查策略以优化性能。每个GC周期都会检查第0代对象。大约1/10的GC周期检查第0代和第1代对象。大约1/100的GC周期检查所有的对象。重新思考Finalization的代价:需要Finalization的对象可能比不需要Finalization在内存中停留额外9个GC周期。如果此时它还没有被Finalize,就变成第2代对象,从而在内存中停留更长时间。
五、GC的优化
1、不要在Unity里面的Update、FixedUpdate等方法去实例化对象
2、我们定义集合的时候例如List,我们要指定集合的大小
3、在运行时创建字符串使用stringbuilder去代替string
4、我们便利数组、集合的时候用for循环代替foreach
5、尽量避免装箱的问题,会产生一些不必要的堆分配
6、实例化的时候我们可以用对象池的概念
7、场景切换的时候手动调用垃圾回收机制,进行占用内存的释放
8、移除Debug输出的使用
9、使用协程把yield return new里面的new去掉
六、总结
这只是个人对于GC回收机制和优化的一些理解,后期还会继续补充、总结!