GC(Garbage Collector,垃圾回收器)是一种自动回收内存的机制,释放已经不再使用的对象的内存空间。
在.NET平台中,我们的托管代码一般都不再关心内存的管理,一切都有CLR(Common language Runtime)去帮我们完成了。当我们开辟内存空间用来创建对象时,使用new关键字,这时CLR会分配一块内存存放对象,大部分时候,我们都不用自己去释放内存空间,而是由CLR在某个适当的时候帮我们释放掉。
为什么要GC?
1.创建新对象开辟内存空间,在使用完后需要释放内存,提高性能
2.避免开发人员直接操作内存,提高安全性
GC的过程
我们运行.NET程序后,OS Loader首先识别出IL,然后会加载CLR的核心库,进行一系列的必要处理后,CLR来到我们编写的代码入口处执行。
当我们的在代码中使用new操作符创建class时,CLR便在叫作GC堆(GC Heap)的内存区域上分配一块内存存放我们的对象,若对象的Size超过85K字节时,考虑到性能原因,将对象创建在LOH(Large Object Heap)上而不是GC堆上【注1】,若我们在class中定义了析构函数来释放非托管资源【注2】,则CLR会在一个叫做终结器队列(Finalizer Queue)的地方添加一个指向该class的项。
我们的程序在运行的过程,在某个时候需要进行垃圾回收了【注3】,首先GC会暂时挂起所有线程,然后确定对象引用的roots【注4】,并根据引用关系创建出由roots出发可以达到的对象形成的对象图,这些对象暂时还在使用,而那些已创建的却不在对象图中的对象则是不可达到的,也就是垃圾了,属于要回收的对象。随后将仍然使用的对象移动到存活期更久的区域【注5】,更改区域指针以回收对象,压缩内存去除内存空隙,并修复对移动的仍存活对象的引用指针,对于有析构函数的对象,则第一次回收时不会回收,而是将其在终结器队列中移除,并添加到另一个标为准备终止的对象列表中,另一个GC线程会调用此列表指向的对象的Finalize(),回收非托管资源,然后将项从列表中移除,下一次的GC才会真正回收掉该对象。
注1:对象创建在Heap上的细节
1)为了更高效的进行GC,.NET将GC堆分成了3个代,Gen0,Gen1和Gen2。
2)这3个代只是逻辑上的划分,在内存中,他们的地址是连续的。
3)Gen0和Gen1之和的大小大约是16M(workstation GC模式下)和64M(server GC模式下)。
4)新创建对象Size小于85k位于Gen0上,大于85K的则创建在LOH上。
注2:定义析构函数释放非托管资源
Finalize方法是用来释放对象中使用的非托管资源,他是作为Dispose()方法的一种安全防护措施,即代码中没有显示的调用Dispose()来释放非托管资源时,GC时调用Finalize方法来释放,Finalize方法中并不直接释放非托管资源,而是调用Dispose(false)来释放。自.NET2.0起,C#中不能直接override Finalize方法,是通过析构函数来实现,析构函数在IL中会被解释为:
默认情况下,一个类是没有析构函数的,那么在GC时是不会调用其Finalize()方法的。
注3:GC发生的时机
1)当Gen0的内存使用达到一个阈值时,将引发Gen0的GC,同理Gen1达到时,会Gen0和Gen1同时GC,若Gen2达到时,则会引发Full GC
2)Windows报告内存不足时
3)调用GC.Collect时
4)其他情况:CLR卸载AppDimain,物理内存不足等
注4:确定对象引用根
对象的引用根主要来自于:FInalize Queue,CPU寄存器中的对象指针,全局对象、静态变量、局部对象、函数调用参数等。
注5: GC时对象的转移
1)Gen0 GC时,会将Gen0中存活的对象整体移动到Gen1中,然后压缩Gen1,使Gen1中的内存连续,同理Gen1中移动到Gen2。
2)Gen2 GC时,此时发生的GC也称为Full GC,会回收整个Heap上的对象,Gen2上的对象将不再移动,而是压缩内存空间。
3)LOH中的对象在Full GC时被回收,但其内存不会被压缩,而是使用一个空闲列表free list记录LOH中的空闲空间,对释放出来的空间进行管理。
4)若对象是pinned object,则此对象不能被移动, 会造成内存碎片。