C#下内存管理--垃圾收集

摘要:
章节安排垃圾收集机制的内存管理性能介绍在C#下处理非托管资源时需要强调的几点内存管理介绍对于任何编程语言,堆栈的主要功能是跟踪函数调用之间的数据传输(堆栈上存储的数据类型通常是int,大多数讨论都是关于堆上的内存管理(堆上分配的类型通常是用户定义的引用类型)。另一个缺点是每次创建对象时都必须检查堆内存,而C#托管堆使用连续内存存储。

章节安排

  1. 内存管理简介
  2. 垃圾回收机制
  3. 性能问题
  4. C#下非托管资源的处理
  5. 要强调的几点
  6. References

内存管理简介

      对于任何一种编程语言,内存管理都是不得不提很重要的一块内容,但可惜的是目前为止没有任何一种编程语言对内存管理处理的非常完美,每种语言都在兼顾性能 效率,语法语义易用性等方面折中中有所侧重。例如较之于C#,JAVA等语言C++号称不需要垃圾收集,因为C++本身产生的垃圾很少,诚然这是C++的 优势,这也就是为什么在内存受限或者效率优先的环境下优先考虑C++,但它的缺点也是明显的--程序员必须自己控制内存管理,很容易产生内存泄漏,这同时 也造就了C++很难掌握。感谢摩尔定律吧,它促使了垃圾收集这个概念的出现,但较之C++直接操纵内存释放,再牛逼的垃圾收集算法也无法抹去那一层性能上 的损失。

      在讨论之前我们先明确一点:内存中数据按所处位置不同可以分为栈内存和堆内存,栈的主要作用是追踪函数调用之间的数据传递(栈上所存储 的数据类型通常是int,char,long,指针等内置值类型和struct。注意一点在多线程环境下,每个线程都有自己的栈。)所以栈的内存管理通常 由操作系统负责。而我们所说的内存管理,大都讨论的是堆上内存管理(分配在堆上的类型一般是自定义引用类型:类,接口,字符串,对象实例,C#中委托 等)。关于这一点详情请参照Under  the hood of NET Management。

       内存内存管理从生命周期上来分可以分为三个阶段:内存分配,内存生命周期内管理,内存的释放。每一阶段都与程序的运行效率关系密切,以C++为例,在新版 的C++标准中Unique_ptr取代auto_ptr,move语义,引入右值引用等措施极大地提高了STL的效率(详细信息参考Refereces 中关于C++的链接)。而兼顾讨论内存管理的所有内容有点不现实,本篇主要关注内存的释放,确切来讲是C#的垃圾回收。    

垃圾回收机制       

       首先声明一点所谓垃圾回收,回收的是分配在托管堆上的内存,对于托管堆外的内存,它无能为力。

       讨论垃圾回收机制就不得不提内存的分配,在C运行时堆(C-runtime heap)中,堆是不连续的,我们new一个新的对象时,系统会检查内存,找一块足够大的内存然后初始化对象,对象被销毁后,这块空间会用于初始化新的对 象。这样做有什么弊端?随着程序运行一直有对象生成释放,内存会变得碎片化,这样有新的大的对象要生成时就必须扩展堆的长度,碎片内存无法得到充分利用, 还有一个弊端是每次创建一个对象时都要检查堆内存,效率不高。而C#托管堆采取连续内存存储,新创建对象时只要考虑剩下的堆内存是否足够大就成,但一直生 成对象而不析构会使托管堆无限增大,怎么维护这样一块连续内存呢?这也就引出了垃圾回收机制。托管堆的大小是特定的,垃圾收集器GC负责当内存不够的时候 释放掉垃圾对象,copy仍在使用的对象成一块连续内存。而这就带来了性能问题,当对象很大的时候,频繁的copy移动对象会降低性能,所以C#的垃圾收 集引入了世代和大对象堆小对象堆的概念。   

      所谓大对象堆小对象堆从字面意义就能看出其作用,大对象堆主要负责分配大的对象,小对象堆分配小的。但对象大小怎么确定呢?在.NET Framework中规定,如果对象大于或等于 85,000 字节,将被视为大型对象。当对象分配请求传入后,如果符合该大小阈值,便会将此对象分配给大型对象堆。这个85000字节是根据性能优化的结果确定。值得 注意的是垃圾回收对大对象堆和小对象堆都起作用。那么分大对象和小对象堆作用是什么呢?还是性能,对于大对象和小对象区别对待采取不同灵活的垃圾回收策略 必定比一棍子打死死板的采用同一种策略要好。下面我们讨论一下在SOH和LOH不同的垃圾收集策略:   

      先说一下世代,之所以分世代,是因为在第0代就能清除大部分对象。请注意,世代是个逻辑上的概念,物理上并没有世代这个数据结构。以小对象堆垃圾回收为 例:当一个对象被创建的时候,它被定义为第0代对象,而经历一次垃圾收集后还存余的对象就被归入了第1代对象,同理经过两次或者两次以上仍然存在的对象就 可以看成第2代对象。虽然世代只是逻辑概念,但它却是有大小的,对于SOH对象来说,由于每次垃圾回收都会压缩移动对象,所以世代数越大越在堆底。经历一 次垃圾回收,对象都会被移入下一个世代的内存空间中(下图以小对象堆上垃圾回收为例。对象W至少经过两次垃圾回收而不死,所以放入世代2,X经历了一次垃 圾回收)。而每次一个世代内存达到其阙值,都会引发垃圾收集器回收一次。这么做的好处就是每次垃圾回收器只是回收一个世代的内存,从整体上来看,减少了对 象复制移动的次数。  

       

C#下内存管理--垃圾收集第1张

         以上讨论都是针对于SOH,对于SOH对象来说复制移动较之LOH成本性能要小一点,那么对于LOH呢?复制一个LOH的对象并且清除原来内存位置的字 节,成本相当大,怎么确保它的垃圾回收呢?首先从物理上来说,LOH在托管堆的堆底,SOH在其上,逻辑上讲LOH对象都分配在第二世代,也就是说前边第 0代和第1代的垃圾收集回收的都是第OH对象,这也就解释了为什么前两个世代垃圾回收中允许复制移动对象。但对于第二世代垃圾回收呢?第二代垃圾回收之后 的对象仍是第二世代,其回收时并不移动仍在使用的对象,压缩空间,而只是清除垃圾对象。当一个新LOH对象创建时,它会从堆底遍历寻找LOH中能够满足要 求的内存,如果没有接着向堆顶创建(这个过程和C运行时工作原理一样,所以也存在相同的弊端,LOH堆内存有可能存在碎片)。此时如果堆顶已经超出阙值, 引发垃圾回收器回收内存空间。

         从上边讨论中我们可以总结一下:我们new出一对象时它要么小对象会被放入第0代,大对象会被分在LOH中,只有垃圾回收器才能够在第1代和第2代中“分配”对象,这里所说分配对象是指移动复制对象。

         以上就是垃圾回收机制,有一块最重要的一点没有讨论,就是垃圾收集器GC怎么判断该对象是垃圾对象。关于这一点可以参考链接。

性能问题

         由上边讨论我们可以看出,自动化垃圾回收是需要付出成本的,而世代和大对象堆/小对象堆这些概念的引入是尽可能的降低这一成本。但性能问题不可避免,性能 数据分析可以很好的帮助我们了解避免些许问题,关于性能分析工具及方法请参考References中链接。

C#下非托管资源的处理--Disposable模式

         我们上边说过,垃圾回收器只能收集托管堆的内存,但对于堆外内存比如HWnds,数据库连接,GDI句柄,safeHandle。这样就有一个问题:垃圾 收集器不会确定地运行,其结果可能会使您的对象在上次引用之后很长时间不能被终结。如果你的对象占用了昂贵或稀少的资源(如一个数据库连接),这是不能被 接受的。为了避免无休止地等待垃圾收集器运行,拥有资源的类型应该实现 IDisposable 接口,然后该类型资源的使用方会及时地释放那些资源。Joe Duff在其网站中给了相关注意细节,并提供了一种Disposable模式。可以参考一下链接了解一下,使你的程序写的更加优雅健壮。(MSDN关于 IDisposable例子中也采用这种方法)

         http://www.bluebytesoftware.com/blog/PermaLink.aspx?guid=88e62cdf-5919-4ac7-bc33-20c06ae539ae

要强调的几点

         1. 值得注意的是虽然微软目前不会移动压缩LOH,但是将来可能会,所以如果分配了大型对象并希望确保它们位置不被移动,则应该将其固定起来。

         2. 这篇文章是基于本人理解,有可能有出入,详细信息可以参照链接,并欢迎指正。

References

C++

http://en.cppreference.com/w/

http://zh.wikipedia.org/wiki/C++0x

http://blog.csdn.net/zentropy/article/details/6973411

http://www.codeproject.com/Articles/71540/Explicating-the-new-C-standard-C-0x-and-its-implem#RValueshttp://www.codeproject.com/Articles/101886/Standard-C-Library-Changes-in-Visual-C-2010

C#

http://msdn.microsoft.com/zh-cn/magazine/bb985011(en-us).aspx

http://msdn.microsoft.com/zh-cn/magazine/cc534993.aspx

http://www.bluebytesoftware.com/blog/PermaLink.aspx?guid=88e62cdf-5919-4ac7-bc33-20c06ae539ae

性能问题和多语言交互

http://msdn.microsoft.com/zh-cn/magazine/ee309515.aspx

http://msdn.microsoft.com/zh-cn/magazine/cc163528.aspx

http://msdn.microsoft.com/zh-cn/magazine/cc163316.aspx

http://msdn.microsoft.com/zh-cn/magazine/cc163392.aspx

垃圾收集发展史:

http://blog.csdn.net/KAI3000/article/details/314628

http://blog.csdn.net/hellothere/article/details/2115422

http://blog.csdn.net/hellothere/article/details/2245734

免责声明:文章转载自《C#下内存管理--垃圾收集》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇[Unity3D]Unity3D游戏开发之刀光剑影特效的实现RE-攻防世界 T4 simple-unpack下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Canopy聚类算法

一、概念     与传统的聚类算法(比如K-means)不同,Canopy聚类最大的特点是不需要事先指定k值(即clustering的个数),因此具有很大的实际应用价值。与其他聚类算法相比,Canopy聚类虽然精度较低,但其在速度上有很大优势,因此可以使用Canopy聚类先对数据进行“粗”聚类,得到k值后再使用K-means进行进一步“细”聚类。这种Can...

自动化中Java面试题

1、面向对象的特征有哪些方面?答:面向对象的特征主要有以下几个方面:- 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。- 继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变...

JS-鼠标、键盘事件及事件对象/event

事件对象/event event包含了发生事件的所有信息内容,不如:发生事件类型,发生事件的元素,键盘按下状态等等 事件对象的创建,当事件发生时,游浏览器帮我们创建的,并通过参数的形式传递给事件处理程序的 事件获取方式 //IE window.event; ​ //非IE arguments[0]; //事件处理程序的参数,第一个参数默...

avalon最佳实践

最近从angular的QQ群与新浪微博拉了许多人来用我的avalon,成为第一批登上方舟,脱离DOM苦海的人。短短三个月内,5群的朋友也搞出几个切实实行的案例了。为应对粉丝们高益高涨的热情,遂放出此文章。 avalon的数据绑定需要经过扫描才能起作用,框架自身会在domReady时进行一次扫描,通过ms-include加载子模板时,也会对当前模板进行一次扫...

Spring Boot @Component注解下的类 @Autowired 为null

Spring Boot @Component注解下的类 @Autowired 为null【原文】 @Component public class ComponentClass { @Autowired private JedisClient jedisClient; public static Co...

关于项目架构的一些浅谈

         最近,一直在学习和摸索关于项目架构的东东。或许说架构说得有点太大。但是还是暂且用着吧。 也看看过几个高手关于三层架构和MVC模型的文章,觉得很多东西的理解和自己的不是很一样。但是自己确实没有他们研究的深入,所以也不妄加评论。         在这里想说的是,自己幼稚的观点欢迎各位砸砖;自己绝对的言语只是针对自己的想法。         我...