CLR内存分配

无论是C#、VB.NET或是Java,这些所谓的高级编程语言实际上都致力于解决C++中常见的内存泄露问题,方法很简单 – 那就是剥夺程序员对于物理内存空间的直接控制权,重新虚拟一层“可控”的内存空间,并由框架(.NET的CLR以及Java的JVM)自行处理内存问题。这样虽然牺牲了一点点性能以及少许灵活性,但大大提高了安全性,更不用说框架封装所带来的便利。

类似于C++,CLR中动态分配的内存存在于一个叫做“托管堆”的地方。程序集初始化时,CLR在物理内存中划出一块作为进程的托管堆,托管堆类似于数据结构中的栈,它维护一个指针,指向下一块可被分配的内存。当使用new操作符声明新对象时,CLR首先计算对象的字节大小,然后再加上一个对象类型指针和一个同步块索引,然后检查托管堆是否拥有足够的可用空间,如果空间足够,则托管堆内部指针向后移动相应距离。

CLR垃圾回收

托管堆内存不足以分配新对象时(或主动调用GC),CLR便会发起垃圾回收。垃圾回收有许多不同的实现方法,例如COM中采用的引用计数,但引用计数无法处理循环引用的问题,即对象AB互相引用,则引用计数永远不可能为0,从而引发内存泄漏。
.NET和Java都采用引用跟踪算法,即将对象变量看作“根”。在垃圾回收时CLR挂起整个线程以防止垃圾回收过程中的状态变化,CLR先将托管堆中所有对象标记为垃圾(将同步索引的某一位置为0),然后以变量为根,向下搜索变量引用的对象和对象的成员所引用的对象,如果变量非NULL,则引用的对象被标记为非垃圾,如果对象已被标记为非垃圾,cLR跳过检查该对象的成员字段,防止循环引用造成的死循环。
标记完成后,CLR将所有非垃圾对象整合到一段连续的内存空间并移动内部指针,这样可以解决传统C++中的内存碎片化问题。但整合内存意味着对象的地址发生变化,因此垃圾清理后必须恢复变量指向的对象的地址。

GC的性能优化

如果每次都检查并移动托管堆中的所有对象,那么会造成比较大的性能开销,因此.NET和Java都采用了基于“代”的回收机制。“分代”的原理很简单,如果某对象经过一次垃圾回收后仍存活,则代数提高一代,而垃圾回收时优先回收第0代,理由是活的越久的对象越有可能继续存活,因此优先回收第0代有更大可能回收更多的空间。
CLR将托管堆对象分为0、1、2三代,CLR对三块内存空间采取了动态负载的方案。垃圾回收时,如果第0代大部分对象都是垃圾,此时垃圾回收的代价很小(仅需要复制少量内存块以及修改少许指针),CLR会相对分配较少的空间给第0代;反之,CLR分配较大的内存空间,以减少垃圾回收的次数。此策略也适用于1、2代。

大对象

CLR将对象分为大对象和小对象,大对象是大小超过85000字节左右的对象,大对象与小对象并不在同一内存空间分配,且初始即为第2代,因为在内存中移动大对象的代价过高。

Finalize与Dispose

当程序集调用非托管的本机资源对象(如File或Mutex)时,CLR只能回收托管内存,造成本机内存的泄露。因此CLR为封装的本机资源对象设计了Finalize方法,拥有该方法的对象在GC执行之后才会被CLR调用该方法,如果方法内有访问对象字段的代码,则此对象的内存并不能立即被回收,这导致对象活得更久,增加了内存消耗。Finalize被设计为只能由CLR调用,托管内存由CLR负责清理,因此Finalize负责清理非托管资源,例如文件句柄。
然而Finalize只有当对象被标记为垃圾且在GC执行后才可能被调用,并且调用时间无法确定,因此有必要设计另一种由开发者精确控制的清理机制,对此CLR设计了IDisposeable接口,接口暴露Dispose方法,实现接口的对象可以由开发者主动调用Dispose方法立即执行非托管资源的清理。实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DisposablClass : IDisposable
{
//标记是否已回收
bool disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); //移除Finalize调用
}
~DisposableClass()
{
Dispose(false);
}

private void Dispose(bool disposing)
{
if(disposed) return; //防止多次执行
if(disposing)
{
//TODO:释放本对象的托管资源
}
//TODO:释放非托管资源
disposed = true;
}
}

内部的Dispose(bool)参数用来标识被Dispose还是Finalize调用,如果是Dispose,则需要自己清理托管资源,同时移除GC对本对象的Finalize调用。如果是Finalize,则托管资源已由GC处理,只需要清理非托管资源。
调用非托管资源的对象类设计原则:如果类实现了Finalize,则也应实现可控的Dispose方法,并在方法中移除GC对Finalize的调用;反之,实现了Dispose方法也应实现Finalize,以防止开发者忘记调用Dispose。

 评论




载入天数...载入时分秒...  |  总访问量为
Powered by Github and MarkdownPad 2

--------------------------- 别拉了,我是有底线的>_< ---------------------------