内存管理-垃圾回收

做.NET开发的应该都知道,CLR为我们提供了GC(Garbage Collector),垃圾回收机制,使我们在大多数情况下不用刻意去管理内存。

但是GC是怎么工作的,是如何工作的,还是有必要了解一下。

在学习GC之前,有必要了解GC的工作对象。

值类型存放在线程栈上,线程栈是每次调用都会产生,用完自己就会释放。

引用类型存放在堆上面,全局共享一个堆,空间有限,所以才需要垃圾回收。

一、托管资源和非托管资源

1.托管资源

由CLR管理的存在于托管堆上的称为托管资源,托管资源的回收工作是不需要人工干预的,由.NET运行库在合适时机调用垃圾回收器进行回收。

2.非托管资源

非托管资源指的是.NET不知道如何回收的资源,最常见的一类非托管资源是包装操作系统资源的对象,例如:读写文件、窗口、网络连接、数据库连接等。这类资源需要手动实现Finalize()或Dispose()方法释放资源。

二、创建一个对象经历了什么

在进程初始化时,CLR要保留一块连续的地址空间,这个地址空间就是托管堆,托管堆还维护着一个指针NextObjPtr,它指向下一个对象在堆中的分配位置。

NextObjPtr是托管堆所维护的一个内存指针,指示下一个对象分配的内存起始地址,它会随着内存的分配而不断移动(当然也会随着内存垃圾回收而发生移动),永远指向下一个空闲的地址。

在IL指令创建对象时,经历了以下步骤:

1、计算类型(及其所有基类型)的字段需要的字节数;

2、对象开销所需字节数(两个开销字段:类型对象指针、同步块索引);

3、CLR检查保留区域是否能够提供分配对象所需的字节数,如果托管堆有足够的可用空间,对象会被放入,调用构造函数、返回对象的地址、NextObjPtr指针指向下一个地址;

 

托管堆将对象所需的字节数加到NextObjPtr指针中的地址上来检测这种情况,如果结果值超过了地址空间的末尾,表明托管堆已满,必须执行一次垃圾回收。

三、垃圾回收

1.垃圾判定

根(Roots)

类中定义的任何的静态字段、方法的参数、局部变量(仅限引用类型变量)等都是根,根是CLR在堆之外可以找到的各种入口点。

垃圾回收器会去检查所有的应用程序根,遍历每个根所引用到的对象,将其标记为活动的(live ),所有的根对象都检查完之后,有标记的对象就是可达对象,未标记的对象就是不可达对象,不可达对象就是回收的目标。

垃圾回收过程

CLR开始GC时,首先会暂停进程中所有的线程,这样可以防止线程在CLR检查期间访问对象并更改其状态。

(1) 标记

标记的过程,其实就是判断对象是否可达的过程,即将该对象的同步块索引中的位设置为1,一个对象被标记后,CLR会检查那个对象的根,标记它们引用的对象,如果发现对象已经被标记,就不重新检查对象的字段,避免因为循环引用而产生的死循环。当所有的根都检查完毕后,堆中将包含可达(已标记)与不可达(未标记)对象;

(2)清除

标记完成后将不可达对象清除。

(3)压缩

垃圾回收器线性遍历堆,寻找到不可达对象的连续内存块,把可达对象移动到这里以压缩堆。这个过程有点类似磁盘空间的碎片整理。

三、垃圾回收算法-分代(Generation)算法

该算法对我们的代码做出了几点假设:

1、对象越新,生存期越短;(对象新老和何时创建有关)

2、对象越老,生存期越长;(对象新老和何时创建有关)

3、回收堆的一部分,速度快于回收整个堆。

托管堆在初始化时不包含任何对象,添加到堆的对象被称为第0代对象,即是那些第一次新构造的对象,垃圾回收器从未检查过它们(GC还没有发生)。

如下图,它分配了5个对象(从A到E),他们都是第0代,过了一会儿,C和E变得不可达(unreacheable,没有根引用它们)

CLR初始化时为第0代对象选择一个预算容量(以KB为单位),如果分配一个新的对象造成第0代超过预算,就必须启动一次垃圾回收

假设对象A到E刚好用完第0代的空间,那么分配新的对象F就必须启动垃圾回收,垃圾回收判断 C和E是垃圾(不可达),就会compact对象D,使之与对象B相邻(形成连续的地址空间),在这次垃圾回收中存活的对象A,B和D,现在成为了第1代对象,此时堆如图:

一次垃圾回收以后,第0代就不包含任何对象了(C和E被清理了,A,B,D变成了第1代对象),那么新对象的分配,就会在第0代中进行,这时候,新分配了对象F到K,这时候,随着应用程序的继续运行,对象B,H和J变得不可达,它们的内存将在某一时刻回收。

假定新分配的对象L会造成第0代超出预算,必须启动垃圾回收。

CLR初始化时,会为第0代对象选择预算。事实上,它还必须为第1代选择预算。

开始垃圾回收时,垃圾回收器还会检查第1代占用了多少内存。在本例中,由于第1代占用的内存远少于预算,所以垃圾回收器只检查第0代中的对象,回看之前的假设,越新的对象活得越短,因此,第0代中包含更多垃圾的可能性最大,能回收更多的内存

由于忽略了第1代中的对象,所以加快了垃圾回收速度,因为你不必遍历拖管堆中所有的对象。

基于代的垃圾回收器还假设,越老的对象活得越长,也就是说,第1代对象在应用程序中很有可能是继续可达的,如果垃圾回收器检查第1代中的对象,有可能找不到多少垃圾,结果是回收不了多少内存,因此,对第1代进行垃圾回收很可能是浪费时间,如果真的有垃圾在第1代中,它将留在那里,此时的堆如图:

所有幸存下来的对象,都成为了第1代的一部分,由于垃圾回收器没有检查第1代,所以对象B虽然已经不可达,但他并没有被回收,同样,在一次垃圾回收后,第0代不包含任何对象(之前分配的对象变成了第1代,不可达的对象被清理),这时候应用程序继续执行,并分配对象L到O,在运行过程中,对象G,L和M变得不可达,此时的托管堆如下:

假设分配对象P导致第0代超过预算,垃圾回收发生。由于第1代中所有对象占据的内存仍小于预算,所以垃圾回收器决定再次只回收第0代,忽略第1代中不可达的对象(对象B和G),回收后的情况如下:

从上图可以看到,第1代正在缓慢的增长,假如第1代的增长也超出了预算,这时候应用程序继续运行,并分配对象P到S,使第0代对象超出了预算,这时候堆如下图:

这时候,应用程序试图分配对象T时,由于第0代已满,所以必须进行垃圾回收,但这一次,垃圾回收器发现第1代占用了太多的内存,以至于用完了预算,所以这次垃圾回收器决定检查第1代和第0代中所有的对象,两代都被垃圾回收后,堆的情况如下:

如果新创建的对象生存周期很短,第0代垃圾也会立刻被垃圾回收器回收(不用等空间分配满)。另外,如果回收了第0代,发现还有很多对象“可达”,并没有释放多少内存,就会增大第0代的预算至512KB,回收效果就会转变为:垃圾回收的次数将减少,但每次都会回收大量的内存。如果还没有释放多少内存,垃圾回收器将执行完全回收(3代),如果还是不够,则会抛出OutOfMemoryException异常。也就是说,垃圾回收器会根据回收内存的大小,动态的调整每一代的分配空间预算,达到自动优化!

四、手动释放内存

对于一些非托管资源,我们需要手动释放内存

1.主动调用Finalize()方法方法

其实这种方法不值得提倡,因为Finalize()方法实际也是由GC调用的,当我们实现Finalize()方法时

A.GC要单独使用一个线程来执行所有对象的Finalize()方法,如果频繁使用析构函数,而且使用它们执行长时间的清理任务,对性能的影响就会非常显著。

B.析构函数的实现会延迟对象最终从内存中删除的时间。没有析构函数的对象会在垃圾回收器的一次处理中从内存中删除,但有析构函数的对象需要两次处理才能销毁:第一次调用析构函数时,没有删除对象,第二次调用才真正删除对象。

C.由于无法确定GC何时会运作,因此可能很长的一段时间里对象的资源都没有得到释放,这对于一些关键资源而言是非常致命的

D.由于负责调用Finalize的线程并不保证各个对象的Finalize的调用顺序,这可能会带来微妙的依赖性问题。如果你在对象a的Finalize中引用了对象b,而a和b两者都实现了Finalize,那么如果b的Finalize先被调用的话,随后在调用a的Finalize时就会出现问题,因为它引用了一个已经被释放的资源。因此,在Finalize方法中应该尽量避免引用其他实现了Finalize方法的对象。

2.IDisposable接口

在C#中,推荐使用System.IDisposable接口替代析构函数。IDisposable接口定义了一种模式,该模式为释放非托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾回收器相关的问题。IDisposable接口声明了一个Dispose()方法,它不带参数,返回void。例如:

public class People : IDisposable
{
        public void Dispose()
        {
            this.Dispose();
        }
}

Dispose()方法的实现代码显式地释放由对象直接使用的所有非托管资源,并在所有也实现了IDisposable接口的封装对象上调用Dispose()方法。这样,Dispose()方法为何时释放非托管资源提供了精确的控制。

3.using

C#提供了一种语法,可以确保在实现了IDisposable接口的对象的引用超出作用域时,在该对象上自动调用Dispose()方法释放资源。该语法使用了using关键字来完成此工作。例如:

using (FileStream fs1 = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite))
{

}

合理的利用资源有助于我们提高程序的性能和安全性~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值