Unity中的System.GC是.Net提供给开发人员用以控制垃圾回收的方式。
但是GC(garbage collection)的发展历程并不是从.Net开始,相比于.Net,GC的历史要悠久的多。虽然在本篇中我们要讨论的是.Net的GC,但是有必要强调的是,GC是一种思想,一种机制。
Garbage consists of objects that are dead.
In tracing garbage collection, the term is sometimes used to mean objects that are known to be dead; that is, objects that are unreachable.
Garbage collection (GC), also known as automatic memory management, is the automatic recycling of dynamically allocated memory. Garbage collection is performed by a garbage collector which recycles memory that it can prove will never be used again. Systems and languages which use garbage collection can be described as garbage-collected.
Garbage collection is a tried and tested memory management technique that has been in use since its invention in the 1950s. It avoids the need for the programmer to deallocate memory blocks explicitly, thus avoiding a number of problems:memory leaks, double frees, and premature frees. The burden on the programmer is reduced by not having to investigate such problems, thereby increasing productivity.
Garbage collection can also dramatically simplify programs, chiefly by allowing modules to present cleaner interfaces to each other: the management of object storage between modules is unnecessary.
It is not possible, in general, for a garbage collector to determine exactly which objects are still live. Even if it didn’t depend on future input, there can be no general algorithm to prove that an object is live (cf. the Halting Problem). All garbage collectors use some efficient approximation to liveness. In tracing garbage collection, the approximation is that an object can’t be live unless it is reachable. In reference counting, the approximation is that an object can’t be live unless it is refrenced. Hybrid algorithms are also possible. Often the term garbage collection is used narrowly to mean only tracing garbage collection.
没用的东西就是垃圾。
在垃圾回收中,垃圾是指那些已知的已经不能再被访问到的东东。
GC(garbage collection)作为自动的内存管理方式,用来收集动态分配的内存,在这里内存作为宝贵的可回收资源,由垃圾回收器负责回收(这里当然没有湿垃圾和有害垃圾)。所有被鉴定为不再使用的资源都会被垃圾回收器收集释放以供再利用。
GC是一种自19世纪50年代发明以来一直被使用的经过尝试和测试过的内存管理技术。CG避免了程序员显示的释放内存,从而避免了内存泄漏,重复释放,过早释放等问题。从而减轻了程序员的负担,提高了生产效率。
GC极大的简化了程序,主要因为使得模块/组件之间能够提供更加干净的接口,因为模块/组件之间不必再做相互存储。
一般来说,要精确的判断一个东东是否还在活着是不可能做到的。所以垃圾收集都是用的有效的近似值。这里介绍两种算法:追踪垃圾回收(tracing garbage collection)和引用计数(reference counting)。追踪垃圾回收认为所有不能再被访问到的都是垃圾。引用计数认为,没有被引用的都是垃圾。混合算法也是可行的。GC通常狭义的指tracing garbage collection。
早在1958年,由鼎鼎大名的图灵奖得主John McCarthy所实现的Lisp语言就已经提供了GC的功能,这是GC的第一次出现。Lisp的程序员认为内存管理太重要了,所以不能由程序员自己来管理。但后来的日子里Lisp却没有成气候,以C为代表的采用内存手动管理的语言占据了上风。出于同样的原因,不同的人却有不同的看法,C程序员认为内存管理太重要了,所以不能由系统来管理,并且讥笑Lisp程序慢如乌龟的运行速度。的确,在那个对每个Byte都要精心计算的年代,GC的速度和对系统资源的大量占用使很多人都无法接受。而后,1984年由Dave Ungar开发的Small talk语言第一次采用了Generational garbage collection的技术,但是Small talk也没有得到十分广泛的应用。
直到20世纪90年代中期GC才以主角的身份登上了历史的舞台,这不得不归功于Java的进步,今日的GC已非吴下阿蒙。Java采用虚拟机(Virtual Machine)机制,由虚拟机来管理程序的运行,当然也包括对GC管理。90年代末期.Net出现了,.Net采用了和Java类似的方法由CLR(Common Language Runtime)来管理这些东东。这两大阵营的出现将人们引入了以虚拟平台为基础的开发时代,GC也在这个时候越来越得到大众的关注。
以上这段文字是引用网上的内容,大量介绍GC的博文里都有这段内容,我也不确定到底哪个才是原创的。
总结下来,因为GC要占用一部分系统资源,所以在内存非常宝贵的时候,GC的代价太高了,即便它确实提供了很多的好处,也因为性能原因而不被接收。
官方文档:GC类 .Net Framework4.8
上面的链接是MSDN的狗翻中文版,我建议还是看英文版更容易理解。
垃圾回收器是用来控制托管内存的分配和释放的公共语言运行时组件。垃圾回收的执行:
垃圾回收的工作内容包括:
在垃圾回收时,如果一个对象被一个或多个其他对象引用,则该对象不会被回收。但是被非托管代码引用的对象,垃圾回收器是无法识别的,这时候可以通过KeepAlive方法保证该对象不会被垃圾回收器清理掉。
如果对象中有非托管资源(比如文件句柄,数据库连接等,所有实现了Dispose方法的类型,都是非托管资源),虽然垃圾回收器可以管理该对象,但是却不知道该怎么处理这里非托管资源,这时候就需要提供一个终结器finalizer。终结器通过重写Finalize方法实现,C#和C++中就是析构方法。要注意的是,如果一个对象拥有析构方法,那么要彻底回收这个对象,需要两次垃圾回收。因为在第一次垃圾回收时,拥有析构方法的对象只会执行析构方法,第二次垃圾回收时才会被释放。
GC在默认情况下是系统自动执行的,系统会在它认为的最佳时机自动进行回收,开发人员通过Collect方法强制垃圾回收,如果调用的时机不得当,反而会增加不必要的开销。所以,GC是个好东西,但不要乱用。
.NET引入了对象老化和代的概念(Object aging and generations):
程序可能使用几百M、甚至几G的内存,对这样的内存区域进行GC操作成本很高,分代算法具备一定统计学基础,对GC的性能改善效果比较明显。将对象按照生命周期分成新的、老的,根据统计分布规律所反映的结果,可以对新、老区域采用不同的回收策略和算法,加强对新区域的回收处理力度,争取在较短时间间隔、较小的内存区域内,以较低成本将执行路径上大量新近抛弃不再使用的局部对象及时回收掉。分代算法的假设前提条件:
Heap分为3个代龄区域。如果Gen 0 heap内存达到阈值,则触发0代GC,0代GC后Gen 0中幸存的对象进入Gen1 heap内存。如果Gen 1的内存达到阈值,则进行1代GC,1代GC将Gen 0 heap和Gen 1 heap一起进行回收,幸存的对象进入Gen2 heap内存。
2代GC将Gen 0 heap、Gen 1 heap和Gen 2 heap一起回收,Gen 0和Gen 1比较小,这两个代龄加起来总是保持在16M左右;Gen2的大小由应用程序确定,可能达到几G,因此0代和1代GC的成本非常低,2代GC称为full GC,通常成本很高。大致上来讲.NET应用运行期间,2代、1代和0代GC的频率应当大致为1:10:100。
网上所有关于强制垃圾回收的使用,都是粗暴的调用Collect无参方法,无参方法会执行full GC,在回收更彻底的同时,也意味着更高的开销。如果存在经常性的生命周期很短的对象回收,可以考虑使用GetGeneration获取到该资源的代龄,然后通过Collect(int)方法进行回收,参数的意思是回收从0代到指定代的资源。
如果要问,两个应该被回收的对象,存在相互引用的关系,这时候垃圾回收还会执行吗?当然会执行了!这里就要提到root机制,通过root扫描,如果对象不再被root引用,那么即便有其他对象引用,也是要被回收的。
某个游戏资源可能会被很多对象同时引用,这就可能导致Resources.UnloadUnusedAssets()方法无法释放。所以,在卸载无用资源前,首先确保C#已经完成了垃圾回收。雨松MOMO说,有时候进行一遍垃圾回收是没用的,最好调用两边GC()和Resources.UnloadUnusedAssets()。但是具体原因不清楚。
雨松MOMO提供的GC组件,个人觉得使用起来不太方便。在这里提供一个自己用的版本
public class GCTool
{
private static GCTool instance;
public static GCTool Instance
{
get
{
if (instance == null)
{
instance = new GCTool();
}
return instance;
}
}
private GCTool()
{
}
public void UnloadUnusedAssets(Action callback,MonoBehaviour behaviour)
{
behaviour.StartCoroutine(Unload(() => { behaviour.StartCoroutine(Unload(callback)); }));
}
IEnumerator Unload(Action back)
{
System.GC.Collect();
AsyncOperation operation = Resources.UnloadUnusedAssets();
while (!operation.isDone)
{
yield return null;
}
back?.Invoke();
}
}
使用起来更方便一些。
说了这么多,说一点最重要的,就像在GC刚诞生的时候关于内存管理该由谁来做的问题一样,都是有利有弊的。使用GC.Collect()清理内存是以进行GC时的复杂工作带来的性能损失为代价的,不当的GC使用,只能是得不偿失。当你不能确定这样的代价是否值得的时候,就不要Collect!更不要频繁的Collect!总之,能不用就不用!