C#中的内存管理(三)垃圾回收

想要理解C#中的垃圾回收是一件很困难的事情,而想要通过三言两语讲清楚,以我现在的理解,怕是可望不可及。

但我还是会尽力写出来,算是对自己学习的一个总结。

C#中的数据类型包括值类型和引用类型,而引用类型的存储会分为两个步骤,首先在内存栈上分配变量的引用地址(内存地址),然后这个地址指向内存堆上变量的实际大小的地址块。如下:

图1

实际上,程序运行后,CLR会创建一个独立的线程,这个线程专门对程序运行过程申请的资源进行管理,在必要的时候进行垃圾回收。

垃圾回收开始执行时,它假设堆中所有对象都是垃圾。换句话说,它假设线程栈中没有引用了堆中对象的变量,没有CPU寄存器引用堆中的对象,也没有静态字段引用堆中的对象。

垃圾回收的第一个阶段也称为标记阶段。在这个阶段,垃圾回收器沿着线程栈以检查所有引用地址,如果堆上存在该引用地址指向的内存块,垃圾收集器将该对象设置为“已标记”,反之,则是“未标记”,也称之为对象不可达。查找所有引用地址后,垃圾收集器会认为不可达的对象是垃圾,它们占用的内存可以回收。

垃圾回收的第二个阶段,即压缩阶段。之所以称之为压缩阶段,是因为垃圾收集器的一个重要作用就是压缩内存,以保持内存块的连续性。

如图1中,内存块B在第一阶段变得不可达,垃圾回收后,B会清空,这就导致内存块A和C的不连续,从而造成内存的存储浪费。为了避免这种情况,垃圾回收器在清空B后,会将C,D,E,F内存块向上移动,同时,指向这些对象的指针也会重新指向新的地址。

为了提升应用程序性能和垃圾回收效率,垃圾回收器引入了“代”的机制。一个基于代的垃圾回收器做出了以下几点假设:

1、对象越新,生存期越短。

2、对象越老,生存期越长。

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

现在我们来了解以下代的工作原理。

如下图,一个新启动的应用程序,它分配了5个对象(从A到E)。这5个对象我们称之为第0代。

过了一会儿,对象C和E变得不可达。

CLR初始化时,它会为第0代对象选择一个预算容量,假定为256KB(实际容量可能会自动调整)。如果此时分配了新对象F到K,当分配F后,第0代的容量超过256KB,就必须启动一次垃圾回收,垃圾回收器判定对象C和E为垃圾,因此会压缩对象D,使其与对象B相邻。这时候,垃圾回收后存活的对象A、B、D、F被认为是第1代对象。而G到K则被认为是第0代,如下:

注意:F对象分配后才启动的垃圾回收,所以F和A、B、D一样。

同样,垃圾收集器会为第1代选择一个预算,假定第一代选择的预算是2MB。一段时间后,对象B,H和J变得不可达。

不久后,应用程序又申请了对象L到O,分配对象L后,第0代超过预算,继续分配M之前,必须进行一次垃圾回收。值得注意的是,B同样不可达,但由于B所在的第一代没有超过预算,所以此时不会对B进行回收。只回收超过预算的第0代的H、J。这样做的目的是因为回收堆的一部分,远远快于回收整个堆。

再次回收并压缩后堆中对象如下:

一段时间后,D、F、M对象不可达。应用程序又申请了对象P、Q、R。

同样,位于第一代的D、F对象不回收,只回收M。并将第0代的幸存者L、N、O归入第一代。此时,L归入第一代后的容量超过预算的2MB,必须对第一代进行一次垃圾回收。第一代中不可达的对象B、D、F回收,并将第一代中的幸存者存入第二代。如下:

至此,垃圾回收已经有了三代,CLR的托管堆支持三代:0代,1代,2代。由以上的规律可以看出,垃圾回收器为每一代预留的容量越大,回收频率越小,但是回收效率也会越小。所以,垃圾回收器在执行过程中回自动更改每一代的预算容量,以使得回收效率更高。实际的工作中,垃圾回收频率最高的是第0代的垃圾回收,据微软的性能测试表明,对第0代执行一次垃圾回收,所花的时间不超过1毫秒,所以多数情况下,垃圾回收的效率还是很高的。

以上是介绍了垃圾回收的机制。熟悉了垃圾回收的机制,我们就可以着手分析实际工作中遇到的问题。

正常情况下我们申请的内存是不需要理会如何释放的,但我们又会常常遇到需要手动终结的对象。如文件、网络连接、套接字、数据库连接、互斥体等对象。这些对象在创建时,会包装一些垃圾回收器无法回收的资源(如本地资源)。所以,为了解决这个问题,垃圾回收使用了“终结”的机制。

任何包装了本地资源(如文件、网络连接、套接字、数据库连接、互斥体)的类型都必须支持终结操作。简单的说,类型必须实现一个命名为Finalize的方法。垃圾回收器在回收之前,会调用对象的Finalize的方法。如下:

internal sealed class SomeType
{
	//这个一个Finalize方法
	~SomeType
	{
	//这里执行清理工作,主要是关闭连接资源或文件句柄
	}
}

注:语法上和C++的析构函数很相似,实际上不同。C++中的析构函数释放函数指针时执行,这里,只会在垃圾回收器回收对象之前执行,而且我们无法预知什么时候执行。

幸运的是,C#中的非托管对象都实现了这个终结方法,所以,一般情况下我们是不需要自己实现该方法的,要知道,实现该方法无疑会降低垃圾回收的效率。

由于Finalize方法不是公共方法,所以类的用户不能显示调用它,更不能保证何时调用它。那么,有没有一种方式能够显示的释放本地资源呢?

答案是肯定的,任何实现IDisposable接口的对象,都能够通过Dispose方法显示的释放本地资源。重申一遍,这里是指资源,不是堆中的内存块。一般实现了Dispose方法的对象会关闭Finalize方法。

最后,垃圾的强制回收:GC.Collect();释放第0代、第1代、第2代的所有可回收对象(对象中如果存在Finalize方法,则回收前执行)。这样就保证了所有内存和资源的释放。但是一般不建议这样操作。


 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值