.NET的GC垃圾回收机制的工作原理
对学习的知识进行理解,消化,归纳。沉淀出自己的知识体系。第一次写博客,文中如果有错误的地方,希望能够帮忙指出修正,避免误导广大的人民群众,非常感谢!
如需要转载,请注明出处,谢谢!
一、GC是什么
GC,垃圾回收器,用于自动回收托管堆中无效对象的内存,释放内存空间。
1.1 为什么需要自动回收
(1)为什么需要自动回收内存,而不是让开发人员手动管理内存呢(例如C或者C++的开发人员),这是有原因的,接触过这两种开发语言的人估计都知道以下两点原因:
- 开发人员忘记释放内存导致内存泄漏。
- 试图使用已经释放的内存导致程序错误和安全漏洞。
这两种BUG比其他大多数的BUG都要严重,我们无法预测它们的后果或发生的时间。
1.2 为什么需要释放内存
(2)至于为什么需要释放内存这个就不用解释了吧,计算机的资源是有限的,如果要等到程序关闭的时候再去释放程序占用的内存,那么系统有再多的内存也不够用。程序在运行过程中 ,有很多对象分配内存空间后,使用一次就不再使用了,这不仅占用内存空间,导致内存溢出,还可能影响程序的性能。
1.3 什么是托管堆
(3)什么是托管堆。这个是引用类型对象的内存空间,可以这么说吧,值类型的内存一般在线程栈中,引用类型的内存在托管堆里。线程栈的内存按照对象的先进后出管理,而托管堆的内存是交给GC自动管理,我们一般不知道托管堆中的对象内存会在什么时候被释放。
二、GC的工作原理
2.1 托管堆的资源分配
CLR 要求所有对象都从托管堆分配。进程初始化时,系统分配一个地址空间区域作为托管堆,用一个指针NextObjPtr来指向下一个对象在堆中的分配位置,如图2-1
C#创建对象时,会执行以下步骤:
- 计算类型所需要的字节数。
- 加上每个对象都有的开销的字节数。每个对象都有两个开销字段:类型对象指针和同步块索引
- 检查托管堆中是否有足够的空间,如果有,则在指针NextObjPtr指向的地址放入对象,清零所分配的空间,调用类型构造器,new操作符返回对象引用。指针NextObjPtr移到下一个对象分配的地址。
2.2 垃圾回收算法
- 当GC进行垃圾回收时,首先会暂停进程的所有线程,防止GC在检查对象的时候被改变对象状态。
- GC会检查程序所有的变量,如果它指向的对象(堆)的同步索引标记为1,如果要被指向的对象的字段也有指向另一个对象,则另一个对象也会被标记为1,如果对象已被标记,则停止,避免死循环。
- 检查完毕后,托管堆中的对象要么被标记为1,要么没有被标记,没有被标记的对象就可以被GC进行回收。
- 托管堆的一些对象被删除后,会造成空间的碎片化问题(即不连续),这时GC会对托管堆进行压缩,其实就是重新对托管堆里面的对象进行移动,使之在地址上连续。如D对象前面的C对象被回收了,那么D对象的地址会向前移动C对象所占用的字节数。
- 托管堆中指针NextObjPtr重新指向最后一个幸存对象之后的位置
- 恢复应用程序的所有线程,GC工作完成。
2.3 基于代的垃圾回收器的性能提升
CLR 实际的GC是基于代的垃圾回收器,并不是每一次的垃圾回收都会检查托管堆里面所有的对象,它会对程序做了以下的假设:
- 对象越新,生存期越短。
- 对象越老,生存期越长。
- 回收堆的一部分,比回收整个堆快。
事实上我们的程序也正是如此,GC把托管堆分成三部分,分别是第0代、第1代和第2代,程序一开始没有经过GC回收的时候,程序内所有的对象均为第0代。第0代经过一次GC回收而没有被回收的对象升级为第1代,第1代经过一次GC回收而没有被回收的对象升级为第2代,最高代为第二代。
GC会优化只检查第0代的对象,当第1代跟第2代累积达到预算的时候才会检查回收(其他特殊情况例外)。
如图:
最分配F对象时,发现超出了第0代的预算,则启动GC回收。
回收第0代对象并进行压缩,调整第0代预算
达到第0代预算条件,继续进行第0代的回收,第1代对象不检查,加快了检查回收速度
回收第0代对象并进行压缩,调整第0代预算
达到第0代预算条件,未达到1代预算,继续进行第0代的回收,第1代对象不检查加快了检查回收速度
回收第0代对象并进行压缩,动态调整第0代预算
达到第0代预算条件,也达到1代预算,这次将检查第0代与第1代的所有对象进行回收。
以上演示了程序开始到产生第2代对象的过程,这只是一个演示的例子,实际上GC的代预算并没有这么小,达到第2代次数要比这个例子要多得多。
关于预算:这里只演示第0代的预算动态调整,其实第1代与第2代也是会进行启发式算法进行动态调整。调整原理:第0代:进行第0代回收时,如果发现没有多少内存被回收,则增加第0代的预算,减少回收频率;如果几乎所有的对象都是垃圾,则减少第0代的预算,增加回收频率,从而提升应用程序的总体性能。垃圾回收器检查有多少内存被回收,有多少幸存对象来动态调整预算。
大对象 :有另外的空间进行分配,并分配就默认为第2代。大对象不进行压缩,代价过高,未来版本可能压缩。
Markdown将文本转换为 HTML。
内存溢出:如果第0代没有回收到足够的内存分配给新对象,那么会执行一次完整的回收,如果还是不够,就抛出OutOfMemoryException异常。
2.4 垃圾回收的触发条件
正常情况下, CLR 检测达到第0代预算触发进行一次GC。但下列情况也会进行:
- 代码显示调用System.GC的静态Collect方法
- Windows报告低内存情况
- CLR正在卸载AppDomain
- CLR正在关闭
不再试图压缩或释放内存,Windows将回收进程的全部内存。
三、其他
GC的一些其他内容,暂时还不知道怎么描述,以后有机会再作补充。
如终结的内部工作原理,其实就是Finalize方法对对象回收的一些影响。
如需要特殊清理的类型例如:System.IO.FileStream,等需要本机资源的类型。
如强制垃圾回收,就是对这个方法的一些讲解,也没有什么特殊的,我觉得就没有必要在这里说了。
如果想详细了解GC的工作原理,可以参考《CLR via C#》一书。