The wind call my name

用知识和思考来丈量世界
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

《Programming .Net Components》学习笔记(七)

Posted on 2007-05-10 12:49  徐鸿翼  阅读(620)  评论(0编辑  收藏  举报

传统上,大部分在非特定业务逻辑实现中的缺陷是源于内存管理和对象生命周期方面的问题。这些缺陷包括内存溢出,循环引用计数,错误地释放对象,错误地自由分配内存,访问已释放的对象和访问尚未被分配的内存或对象等等。为了缩小技术上的差距和增加代码质量,简化组件开发。.NET免除了开发者几乎所有对于对象,内存释放和对象生命周期管理上的内存分配负担。.Net对于内存和对象生命周期管理的解决方案和它对于编程模型的实现是组件开发者需要掌握的。

 

一、托管堆

.Net组件不会通过上层操作系统来分配原始内存作为替代,寄宿于.NET中的每个物理进程,.NET在运行时会预先分配一个特殊的堆,称作“the managed heap(托管堆)”。 这个堆可以当做传统的操作系统中的堆来使用:为对象和数据存储来分配内存。在任何时候,在一个类上使用new操作符:

   MyClass obj = new MyClass();

.NET就会分配内存到托管堆中。

托管推只是一长段内存。.NET维护一个在托管堆中下一个可使用地址的指针。当.NET被要求创建一个对象时,他就会为对象分配请求的空间并更新指针。这种分配方法相比原始内存分配会成数量级地提高速度。在非托管环境中(如C++),对象被分配到本地操作系统堆中。操作系统通过一个可用内存块的链表来管理内存。每当操作系统分配内存时,就不得不遍历链表来寻找足够大的内存块。过不了多久,内存就会变得零碎而且导致可用内存块的链表变得越来越长。内存碎片导致性能问题的一个主要来源,为分配请求遍历链表,整合新增的内存页缺失和磁盘访问消耗都会花费了大量时间。

 

二、传统的内存释放模式

内存释放和对象销毁在.NET中,与原始C++COM也是不同的。在C++中,当一个基于堆栈的对象超出范围时,对象析构器就会被调用:

{//beginning of a C++ scope

     MyClass object;

     //use object;

}//end of scope,C++ calls the object destructor

C++中,当使用delete操作符时,对象析构器也会被调用:

//in C++

MyClass* pObject=new MyClass;

//using pObject,then de-allocating it

Delete pObject;

COM则使用引用计数,它负责与客户端通信的每一个对象的增加和减少计数。新的COM对象创建时,带有一个引用计数。共享对象的客户端不得请求AddRef()来增加计数。当一个客户端处理完一个对象时,它就会请求Release()来减少计数:

//COM pseudo-code

IMyInterface* pObject=NULL

::CoCreateInstance(CLSID_MyClass,IID_IMyInterface,&pObject)

//using pObject,then releasing it

pObject->Realease();

当引用计数到达1时,对象就会自我销毁:

//COM implementation of IUnknown:Release()

ULONG MyClass::Release()

{

   //m_Counter is this class counter

   m_Counter--;

   if (m_Counter == 0)

{

   delete this;

   return 0;

}

//Should return the counter;

Return m_Counter;

}


三、.NET垃圾回收机制

.NET编程中,不像COM一样存在一个销毁对象的范围,.NET也不使用对象引用计数。作为替代,.NET拥有一个先进的垃圾回收机制,当检测到一个对象不会被客户端再使用时就会自动销毁它。因此,.NET须维护代码中对象可访问路径的内存地址JIT编译器编译IL代码时,它会更新一个根列表——应用程序的起始指针,如静态的变量和方法(Main()),或是应用程序运行期间需保持生存的全局.NET实体。每个根构成在树状图中的最高级节点。.NET保存了每个分配到托管堆中新对象的内存地址,以及该对象和调用它的客户端之间联系的内存地址。无论何时,当一个对象被分配到内存中时,.NET都会更新相应对象图表并在图表中添加一个创建该对象的对象引用。同样地,在每次客户端对象接收一个对象引用和一个对象保存一个对象引用到它的成员变量时,.NET也会更新图表。当每次执行路径进入或退出一个范围的时候,JIT编译器也会自动添加代码来更新图表。

 

负责释放不再使用的内存的实体称为“垃圾收集器”。当垃圾回收机制被触发时(通常是托管堆被耗尽时,也可以是被代码显式请求),垃圾收集器就会把在图表中的每个对象当作可回收的垃圾,沿着根遍历每一个图表,寻找“可达对象”。每当垃圾收集器访问到一个对象时,就把它标记为“可达对象”。因为图表中描述了客户端和对象之间的关系,所以当垃圾收集器遍历图表时,他就会知道哪个对象是可达的。可达对象是应保持生存的。不可达对象就被认为是垃圾,即销毁它们不会带来损害。这个算法也同样适用于处理循环引用。当垃圾收集器到达一个以标记为“可达”的对象时,它就不会继续从该标记对象中寻找其他可达对象了。

 

下一步,垃圾收集器会扫描托管堆,并通过压缩堆和用一个可达对象来重写非可达对象的方式来销毁垃圾。垃圾收集器向下搬移托管堆中的可达对象,覆写垃圾,在托管堆末端为新对象提供更多的可用空间。这样,所有非可达对象就被从图表中清除了。


但是,通过向下搬移可达对象的方法来压缩托管堆就意味着客户端对于这些对象的引用现在变成了一个空引用。垃圾收集器补偿措施是,修改在客户端代码中所有对于搬移对象的引用。

 

垃圾收集器面临的另一个问题是,它必须确保应用程序代码不会改变对象图表结构或在清理垃圾期间的托管堆状态。唯一安全的方式是在垃圾回收期间挂起所有应用程序线程。那么,.NET怎么会知道何时可以安全地挂起一个线程呢?例如,线程可能还在分配内存或修改数据结构的动作中。为了处理这个问题,JIT编译器在代码中插入了“safe point(安全指针)”,指向可安全挂起线程的执行路径。垃圾收集器实际上是通过从安全指针除插入一个不同返回地址(一个包含挂起线程请求的地址)来劫持线程。当垃圾回收处理完毕,.NET会发送线程回它本来的返回地址中继续正常执行。


根据原版英文翻译总结的,所以不足和错误之处请大家不吝指正,谢谢:)