c#

位置:IT落伍者 >> c# >> 浏览文章

深入.NET托管堆(managedheap)(1)


发布日期:2021年07月01日
 
深入.NET托管堆(managedheap)(1)

NET的所有技术中最具争议的恐怕是垃圾收集(Garbage CollectionGC)了作为NET框架中一个重要的部分托管堆和垃圾收集机制对我们中的大部分人来说是陌生的概念在这篇文章中将要讨论托管堆和你将从中得到怎样的好处

为什么要托管堆?

NET框架包含一个托管堆所有的NET语言在分配引用类型对象时都要使用它像值类型这样的轻量级对象始终分配在栈中但是所有的类实例和数组都被生成在一个内存池中这个内存池就是托管堆

垃圾收集器的基本算法很简单

● 将所有的托管内存标记为垃圾

● 寻找正被使用的内存块并将他们标记为有效

● 释放所有没有被使用的内存块

● 整理堆以减少碎片

托管堆优化

看上去似乎很简单但是垃圾收集器实际采用的步骤和堆管理系统的其他部分并非微不足道其中常常涉及为提高性能而作的优化设计举例来说垃圾收集遍历整个内存池具有很高的开销然而研究表明大部分在托管堆上分配的对象只有很短的生存期因此堆被分成三个段称作generations新分配的对象被放在generation 这个generation是最先被回收的——在这个generation中最有可能找到不再使用的内存由于它的尺寸很小(小到足以放进处理器的L cache中)因此在它里面的回收将是最快和最高效的

托管堆的另外一种优化操作与locality of reference规则有关该规则表明一起分配的对象经常被一起使用如果对象们在堆中位置很紧凑的话高速缓存的性能将会得到提高由于托管堆的天性对象们总是被分配在连续的地址上托管堆总是保持紧凑结果使得对象们始终彼此靠近永远不会分得很远这一点与标准堆提供的非托管代码形成了鲜明的对比在标准堆中堆很容易变成碎片而且一起分配的对象经常分得很远

还有一种优化是与大对象有关的通常大对象具有很长的生存期当一个大对象在NET托管堆中产生时它被分配在堆的一个特殊部分中这部分堆永远不会被整理因为移动大对象所带来的开销超过了整理这部分堆所能提高的性能

关于外部资源(External Resources)的问题

垃圾收集器能够有效地管理从托管堆中释放的资源但是资源回收操作只有在内存紧张而触发一个回收动作时才执行那么类是怎样来管理像数据库连接或者窗口句柄这样有限的资源的呢?等待直到垃圾回收被触发之后再清理数据库连接或者文件句柄并不是一个好方法这会严重降低系统的性能

所有拥有外部资源的类在这些资源已经不再用到的时候都应当执行Close或者Dispose方法从Beta(译注本文中所有的Beta均是指NET Framework Beta不再特别注明)开始Dispose模式通过IDisposable接口来实现这将在本文的后续部分讨论

需要清理外部资源的类还应当实现一个终止操作(finalizer)在C#中创建终止操作的首选方式是在析构函数中实现而在Framework层终止操作的实现则是通过重载SystemObjectFinalize 方法以下两种实现终止操作的方法是等效的

~OverdueBookLocator()

{

Dispose(false);

}

public void Finalize()

{

baseFinalize();

Dispose(false);

}

在C#中同时在Finalize方法和析构函数实现终止操作将会导致错误的产生

除非你有足够的理由否则你不应该创建析构函数或者Finalize方法终止操作会降低系统的性能并且增加执行期的内存开销同时由于终止操作被执行的方式你并不能保证何时一个终止操作会被执行

内存分配和垃圾回收的细节

对GC有了一个总体印象之后让我们来讨论关于托管堆中的分配与回收工作的细节托管堆看起来与我们已经熟悉的C++编程中的传统的堆一点都不像在传统的堆中数据结构习惯于使用大块的空闲内存在其中查找特定大小的内存块是一件很耗时的工作尤其是当内存中充满碎片的时候与此不同在托管堆中内存被组制成连续的数组指针总是巡着已经被使用的内存和未被使用的内存之间的边界移动当内存被分配的时候指针只是简单地递增——由此而来的一个好处是分配操作的效率得到了很大的提升

当对象被分配的时候它们一开始被放在generation 当generation 的大小快要达到它的上限的时候一个只在generation 中执行的回收操作被触发由于generation 的大小很小因此这将是一个非常快的GC过程这个GC过程的结果是将generation 彻底的刷新了一遍不再使用的对象被释放确实正被使用的对象被整理并移入generation

当generation 的大小随着从generation 中移入的对象数量的增加而接近它的上限的时候一个回收动作被触发来在generation 和generation 中执行GC过程如同在generation 中一样不再使用的对象被释放正在被使用的对象被整理并移入下一个generation中大部分GC过程的主要目标是generation 因为在generation 中最有可能存在大量的已不再使用的临时对象对generation 的回收过程具有很高的开销并且此过程只有在generation 和generation 的GC过程不能释放足够的内存时才会被触发如果对generation 的GC过程仍然不能释放足够的内存那么系统就会抛出OutOfMemoryException异常

带有终止操作的对象的垃圾收集过程要稍微复杂一些当一个带有终止操作的对象被标记为垃圾时它并不会被立即释放相反它会被放置在一个终止队列(finalization queue)中此队列为这个对象建立一个引用来避免这个对象被回收后台线程为队列中的每个对象执行它们各自的终止操作并且将已经执行过终止操作的对象从终止队列中删除只有那些已经执行过终止操作的对象才会在下一次垃圾回收过程中被从内存中删除这样做的一个后果是等待被终止的对象有可能在它被清除之前被移入更高一级的generation中从而增加它被清除的延迟时间

需要执行终止操作的对象应当实现IDisposable接口以便客户程序通过此接口快速执行终止动作IDisposable接口包含一个方法——Dispose这个被Beta引入的接口采用一种在Beta之前就已经被广泛使用的模式实现从本质上讲一个需要终止操作的对象暴露出Dispose方法这个方法被用来释放外部资源并抑制终止操作就象下面这个程序片断所演示的那样

public class OverdueBookLocator: IDisposable

{

~OverdueBookLocator()

{

InternalDispose(false);

}

public void Dispose()

{

InternalDispose(true);

}

protected void InternalDispose(bool disposing)

{

if(disposing)

{

GCSuppressFinalize(this);

// Dispose of managed objects if disposing

}

// free external resources here

}

}

上一篇:.net程序员的盲点(一):ref,out ,params的区别

下一篇:使用C#编写DES加密程序的framework