.NET内存管理原理

      其实.NET开发大部分时候都不需要我们去考虑内存的分配与释放的问题。因为在托管环境中,内存的分配与回收是.NET运行库会自动去做的事情。

但是如果需要写出高效,严谨的代码或者需要进行非托管资源的管理,我觉得还是应该对系统自动给我们做的这些事情有一些深入的了解。之前我对于这方面的了解也是一支半解,甚至有些理解是错误的。这两天认真学习了一下,记录在这里做个总结。

我觉得这方面主要应该包括三个方面:内存的分配,内存的回收,内存的回收的控制

1. 内存的分配

.NET程序中主要有两大类数据类型:值类型与引用类型。.NET Framework会分别为这两种类型的数据类型在线程栈和托管堆上分配内存空间。

 

-值类型

值类型的定义应该是所有从System.ValueType派生的类。Int,Double,Float,Struct,Enum等等均是值类型。

这里有一点疑问,根据MSDN:Int,Double等都是属于Struct,但是在MSDN上找不到直接证据证明,Struct等由System.ValueType派生。

其实这里的关键在于struct是C#提供的关键字,编绎器肯定是针对这个关键字又做了一些额外的事情。每当我想知道编绎器到底为我们做了什么事情的时候,我便会写一段测试代码,编绎后用IL Disassembler打开看一下。用这个方法我们来看一下编绎器对struct关键字做了什么:

源代码:

struct CustomStruct
    {
        string name;
        int age;
    }

编绎器输出:

可以看到,编绎器是把struct翻译成了一个继承自System.ValueType的一个类。

 

在.NET程序中,非成员变量的值类型(局部变量,方法参数等)都会被分配内存到线程堆栈上。由于非成员变量的值类型一般生命周期符合栈的特点(后进先出)。因此其内存的分配与释放也是这样的。

线程栈会有一个栈顶指针,当有一个新的值类型需要申请空间。则从栈顶指针处开始,为其分配所需要的内存空间。栈顶指针往后移到下一个自由空间处。

如果该数据的生命周期结束,则从栈中回收掉其所占用的内存空间,同时栈顶指针向前移动该数据所占用的空间单位。再次指向下一个自由空间处。

 

-引用类型

.NET中大部分都是引用类型(至于准确的定义我想应该是所有从System.Object继承但是不属于System.ValueType的派生类的类型)

引用类型的的内存分配其实包含两部分:用来表示引用类型的变量和引用类型实例本身。

--对于引用类型变量是非成员变量,会遵循值类型的内存分配原则在堆栈中为其分配空间用来保存引用类型实例的引用(一个堆地址)。

--对于引用类型实例,CLR会在堆中为其找到合适大小的内存空间进行分配。

2. 垃圾回收器

对于垃圾回收器的理解主要来源于这篇文章:http://kb.cnblogs.com/page/106720/

这里主要概括一下对这篇文章的一个总结性的理解。

-垃圾回收算法

1. Mark-Compact 标记压缩算法

GC会从所有的Root Object(静态变量或线程正在使用的对象,其实我的理解是静态变量和被线程栈中某个变量所引用的对象)作为入口,递归搜索其所引用的对象。从而最终知道在堆中哪些对象是无法从Root Object到达的(也就是不需要了的,可以被回收掉的)。这些被认为可以回收的对象会继续地行垃圾回收的后续流程。

2. Generational 分代算法

由于堆可能是很大的,如果每次都对整表堆进行垃圾回收过程,系统性能可能会受到影响。.NET的策略是认为比较新的对象需要被回收的可能性会比老的对象大。

因此.NET的分代算法如下:

将整个堆分为3代(0代,1代,2代),0代是比较新的对象,空间也相对较小。GC对0代的回收会比较频繁,对1代和2代的回收操作机率会小一些。每次对0代对回收处理会产生两批结果:

(1)需要回收的对象

(2)不需要回收的对象

对于不需要回收的对象,则将其置于1代。以此内推,对1代的处理有可能会导致某些对象进入2代。(2代所代表的空间是很大的)而每次对1代的回收也会导致对0代的回收,对2代的回收也会导致0代和1代回收。

3.Finalization Queue和Freachable Queue

这两个队列也算是垃圾回收算法中的一个环节。当在堆中为引用类型分配空间时,如果引用类型实现了Finalize方法(析构函数)则对象的指针会被放入

Finalization Queue。当进行垃圾回收的时候如果某个对象确定为需要被回收,GC会去搜索Finalization Queue。如是搜索到该对象具有Finalize方法,则该对象的指针会从Finalization Queue中被移动到Freachable Queue。并且该对象也会被暂时复活。同时GC中会有另一个线程负责去调用该对象的Finalize方法,调用结束之后该对象的结果指针会从Freachable Queue中移除。下一次GC进行垃圾回收的时候,该对象会被立即回收掉。(由此看来,实现Finalize方法会延长内存的回收时间)

-垃圾回收过程

垃圾回收的过程大致可以分为如下几步:

(1) 挂起所有线程

(2) 检测所有需要回收的对象

(3) 回收垃圾对象所占用的内存空间

(4) 对堆内存空间进行压缩(类似碎片整理),以保证堆的内存最大化的连续(提高为对象分配内存空间时的效率)

(5) 由于对堆内内存进行了调整,需更新所有对堆内对象引用的地址。

3. 析构函数与IDisposable接口

析构函数最后都会被编译器翻译为Finalize方法。GC在进行垃圾回收的时候会调用该方法(如果类实现了该方法的话)。

那么如果类型中使用了一些需要手动清理的非托管资源,应该放在这个方法里面就行了。

但是这里存在一个问题,因为GC进行垃圾回收的时间是不确定的。这样就可能导致某些资源已经不需要了,但是由于GC还没有进行回收导致这些资源仍然在内存中。这样导致资源的浪费,也会导致系统的性能问题,甚至可能导致资源使用冲突。

问题存在的原因在于,对资源回收的时间不能控制。因此.NET提供了IDisposable接口供需要手动释放资源的类型实现。

该接口提供一个Dispose方法,可以被使用者显式调用,因此对资源回收的逻辑可以放在这个方法里面。实际开发过程中一般会结合IDisposable接口和析构函数一起来释放非托管资源。(具体实现参考http://kb.cnblogs.com/page/106720/

posted @ 2012-07-29 15:07  self.refactoring  阅读(563)  评论(0编辑  收藏  举报