章节安排
- 内存管理简介
- 垃圾回收机制
- 性能问题
- C#下非托管资源的处理
- 要强调的几点
- 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经历了一次垃圾回收)。而每次一个世代内存达到其阙值,都会引发垃圾收集器回收一次。这么做的好处就是每次垃圾回收器只是回收一个世代的内存,从整体上来看,减少了对象复制移动的次数。
以上讨论都是针对于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://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#RValues http://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