概述
在C#中,提供了和Java一样的垃圾回收机制,当一个对象没有了任何指向其的引用,那么垃圾回收器(Garbage Collector)就会将该块内存回收。与其他语言一样,其管理的是堆内存。
默认情况下,在32位计算机里每个进程都有2GB用户态的虚拟地址。
内存管理方法
C#的垃圾回收器将内存分为三类:第零代内存(空闲堆)、第一代内存、第二代内存
大对象堆也属于第二代内存,所谓的第N代内存就通俗一点来讲即是被整理的次数。GC(Garbage Collector)每次运行都会对内存进行整理,减少碎片,并将无需清理的内存块都往下一代内存移动,尽可能的让空闲堆为空,这样在下次申请内存的时候大概率下只需要使用堆顶指针即可,以此提高效率。当申请大块内存超出了堆顶空间时,GC也会被调用以重整内存。
GC在整理第二代内存及超大对象堆(≥85000字节)时,总是会在后台线程上执行,以减少中断程序的时间。
此外,GC对线程池的垃圾回收也有优化,因为线程池执行的工作是类似的,当一个线程耗尽空闲堆需要整理内存的时候,其他的线程也会受益于该过程。但可预见的是,如果有部分线程内存使用率远高于其他线程时,该方法则可能甚至产生副作用,不过GC会在一定程度上对这些堆进行平衡,以减少不必要的内存整理。
对象的默认初始化即是GC的功劳。
注意,垃圾回收一般只在某一个代中发生,它将会回收当前代及其更年轻代中的对象,因此第二代内存的垃圾回收也称作完整垃圾回收。
GC使用以下信息来确定对象是否需要被回收:
堆栈根(Stack roots) 由JIT编译器及堆栈浏览器提供。
垃圾回收句柄(Garbage collection handles) 指向托管对象的句柄,由用户代码或者CLR提供。
静态数据(Static data) 静态对象跟踪,由应用程序域提供。
回收的时机
系统可用内存低
在堆上对象使用的内存超越了其阈值(该阈值随着程序运行不断自调整)
主动调用
System.GC.Collect()
方法
强引用和弱引用
GC把对象分为强引用和弱引用,一般使用对象的方式,即为强引用。GC保证不会回收任何有强引用的内存。但是有时候可能会因为各种引用关系忘记清理,导致部分内存永远不被回收。
对此,引出了一种弱引用方法,只要弱引用所在内存块运行了垃圾回收,弱引用所占用的内存则会被回收。但是这种方式有可能在内存未使用完毕时就被回收,不过在特定场景下弱引用还是有其道理的,如一张表,其内存占用非常大,但并不是程序的主要部分,如果用户在使用其他功能时仍然在内存中保留如此庞大的表便不那么经济。值得注意的是,弱引用本身开销即不小,所以不应该用在小对象上。
弱引用的使用方式如下:
var myWeakRef = new WeakReference(new DataObj());
因为弱引用可能在使用前就会被回收,所以使用时一定要先检查是否存在:
if( myWeakRef.IsAlive )
非托管资源
由于不是所有资源GC都知道如何回收或者在何时回收,例如本机文件句柄的引用,就必须显式释放。因为GC无法跟踪不位于托管堆上的资源。
C#为此提供了类似析构函数的机制,使用Finalize
方法实现,但C#不同于C++,无法保证析构调用时资源就已经回收(因为GC机制问题)。此外,还有一个IDisposable
接口提供更加类似析构函数的行为,该接口实现有语言级支持,通过这个接口实现的资源释放能保证调用时即释放,但释放后需要通过GC.SuppressFinalize(this)
来告诉GC不需要再为它回收资源。
由于IDisposable
是由程序员调用,所以使用时要小心因为异常抛出导致释放操作被跳过,因此最好配合try/finally
语法使用:
try
{
var theObj = new Obj();
//do somethings
}
finally
{
theObj?.Dispose();
}
还为此提供了语法糖,写法更简单,其行为和上方代码完全一致:
using( var theObj = newObj() )
{
//do somthings
}
后台垃圾回收
对于第二代内存回收,总是由后台垃圾回收(Background garbage collection)实现,服务器和非服务器又有些略微不同。相对的,第零、一代内存叫做前台垃圾回收(foreground garbage collection)。
当前台垃圾回收被执行时,后台垃圾回收和托管的线程都会被挂起,直到其执行完毕。使用这种方式整理内存可以尽量减少线程因为垃圾回收器导致的挂起。
附录
一些有用的资源:
文中图片来自MSDN