.net之GC垃圾回收原理

一、资源

托管资源 :由CLR管理分配和释放的资源,也就是我们直接new出来的对象;

非托管资源:不受CLR控制的资源,也就是不属于.NET本身的功能,往往是通过调用跨平台程序集(如C++)或者操作系统提供的一些接口,比如Windows内核对象、文件操作、数据库连接、socket、Win32API、网络等。

GC垃圾回收主要是帮我们回收 托管资源
对于非托管资源的回收,需要开发人员自己写代码实现回收。在.Net framework中,开发人员通常会把清理这类资源的代码写到Close、Dispose或者Finalize方法中

二、托管堆

.Net CLR把所有的引用对象都分配到托管堆上,那么垃圾回收器是怎么知道一个对象不再使用该回收了呢?

当一个进程初始化之后,运行时会保留一段连续的空白内存空间,这块内存空间就是托管堆,我们暂且理解为从低地址到高地址的连续内存空间。托管堆会记录一个指针,我们叫它NextObjPtr,这个指针指向下一个对象的分配地址,最初的时候,这个指针指向托管堆的起始位置。

应用程序使用new操作符创建一个新对象,这个操作符首先要确认托管堆剩余空间能放得下这个对象,如果能放得下,就把NextObjPtr指针指向这个对象,然后调用对象的构造函数,new操作符返回对象的地址。NextObjPtr随之上移。

当应用程序调用new操作符创建对象时,有可能已经没有内存来存放这个对象了。托管堆可以检测到NextObjPtr指向的空间是否超过了堆的大小,如果超过了就说明托管堆满了,就需要做一次垃圾回收了。
在现实中,在0代堆满了之后就会触发一次垃圾回收。

代:
“代”:在.net CLR中目前是分为三代,可以通过调用GC.MaxGeneration得知
0代:内存最大长度通常为256KB,发生垃圾回收后幸存的对象升级为1代
1代:内存最大长度通常为2 MB,发生垃圾回收后幸存的对象升级为2代
2代:内存长度比较大,发生垃圾回收后幸存的对象依然为2代
0代堆满了触发垃圾回收,一般0代回收后内存依然不够分配给新的对象使用,会继续回收1代、0代;还不够则回收 2代、1代、0代;还不够,那么 new操作符就会抛出OutofMemoryException

三、垃圾回收算法

每个应用程序都有一组根对象,根是一些存储位置,他们可能指向托管堆上的某个地址,也可能是null
.NET中可以当作GC Root的对象有如下几种:
1、全局变量
2、静态变量
3、线程栈上的所有局部变量(JIT)
4、线程栈上传入的参数变量
5、CPU寄存器中的变量
注意,只有引用类型的变量才被认为是根,值类型的变量永远不被认为是根。因为值类型存储在栈中,而引用类型存储在托管堆上。

1. 算法第一步:标记
当垃圾回收器开始运行,它会假设托管堆上的所有对象都是垃圾。
从根对象出发 开始构建一个由所有和根对象之间有引用关系对象构成的图.
如果垃圾回收器发现一个对象已经在图中就会换一个路径继续遍历。这样做有两个目的:一是提高性能,二是避免无限循环。

另外,实现了Finalize方法的对象,垃圾回收器第一次执行时,会被提升到更老的“代”,这会增加内存压力,使对象和此对象的关联对象不能在成为垃圾的第一时间回收掉。
具体执行过程:

先搞清楚两个队列

1)Finalization 队列:终结队列
当应用程序创建一个新对象时,new操作符在堆上分配内存,如果对象实现了Finalize方法,对象的指针会放到终结队列中,暂且称为这种对象为 Finalize对象吧。终结队列是由垃圾回收器控制的内部数据结构。在队列中每一个对象在回收时都需要调用它们的Finalize方法。

2)Freachable队列:需要被执行的Finalize对象(指针)队列
当Finalize对象需要被回收时,垃圾回收器扫描终结队列找到这些对象的指针,当发现对象指针时,指针会被移动到 Freachable队列。
Freachable队列是另一个由垃圾回收器控制的内部数据结构。
程序运行时会有一个专门的线程负责调用Freachable队列中对象的Finalize方法。当Freachable队列为空时,这个线程会休 眠,当队列中有对象时,线程被唤醒,移除队列中的对象,并调用它们的Finalize方法。因此,如果一个对象在freachable队列中,那么这个对象就不是垃圾,且在执行Finalize方法时不要企图访问线程的 local storage。
再次触发垃圾回收之后,实现Finalize方法的对象才被真正的回收。这些对象的Finalize方法已经执行过了,Freachable队列清空了。

2. 算法第二步:清除
所有的根对象都检查完之后,垃圾回收器的图中就有了应用程序中所有的可达对象。
托管堆上所有不在这个图上的对象就是要做回收的垃圾对象了。
挂起所有访问托管资源的线程:采用 安全点挂起或劫持的方式
线性的遍历托管堆,将非垃圾对象向下移动到一起,覆盖所有的内存碎片,同时修改应用程序的根对象使他们指向对象的新内存地址,如果某个对象包含另一个对象的指针,垃圾回收器也要负责修改引用。

如你看到的,垃圾回收会有显著的性能损失,这是使用托管堆的一个明显的缺点。 不过,要记着内存回收操作是在托管堆满了之后才会执行。在满之前托管堆的性能比c-runtime堆的性能好要好。并且,运行时垃圾回收器还会做一些性能优化。

四、结尾

GC的开发人员一直在调整垃圾回收器提升它的性能。代就是一种为了降低垃圾回收对性能影响的机制。垃圾回收器在工作时会假定如下说法是成立的:

  1. 一个对象越新,生命周期就越短
  2. 一个对象越老,生命周期就越长
  3. 新对象之间通常更可能和新对象之间存在引用关系
  4. 压缩堆的一部分要比压缩整个堆要快

.NET的垃圾收集器将对象分为三代(Generation0,Generation1,Generation2)。不同的代里面的内容如下:
  1、G0 小对象(Size<85000Byte):新分配的小于85000字节的对象。
  2、G1:在GC中幸存下来的G0对象
  3、G2:大对象(Size>=85000Byte);在GC中幸存下来的G1对象

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值