深入.NET托管堆(managed heap)(上)

原创 2002年05月27日 09:47:00

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

<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

 

为什么要托管堆?

 

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

 

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

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

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

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

整理堆以减少碎片

 

托管堆优化

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

~OverdueBookLocator()

{

    Dispose(false);

}

 

和:

 

public void Finalize()

{

    base.Finalize();

    Dispose(false);

}

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

public class OverdueBookLocator: IDisposable

{

    ~OverdueBookLocator()

    {

        InternalDispose(false);

    }

 

    public void Dispose()

    {

        InternalDispose(true);

    }

 

    protected void InternalDispose(bool disposing)

    {

        if(disposing)

        {

            GC.SuppressFinalize(this);

            // Dispose of managed objects if disposing.

        }

        // free external resources here

    }

}

[原创]What is &quot;Type&quot; in managed heap?

我们知道,在程序运行过程中,每个对象(object)都是对应了一块内存,这里的对象不仅仅指的是某个具体类型的实例(instance),也包括类型(type)本身。我想大家也很清楚CLR如何为我们...
  • artech
  • artech
  • 2007年06月04日 03:25
  • 323

Unity内存优化(-)托管堆ManagedHeap的优化

很多童鞋在进行内存分析的时候都会遇到下面的问题: ManagedHeap.UsedSize和ManagedHeap.reservedUnUsedSize这两个占用了很大内存,即使手动GC.Coll...
  • cbbbc
  • cbbbc
  • 2016年04月19日 20:23
  • 4203

.Net 托管代码和非托管代码的区别

什么是托管代码(managed code)?       托管代码是一microsoft的中间语言(IL),他主要的作用是在.NET   FRAMEWORK的公共语言运行库(CLR)执行代码前去...
  • avon520
  • avon520
  • 2014年06月11日 16:47
  • 1291

C#垃圾回收和托管堆及堆栈

堆栈和托管堆: 首先堆栈和堆(托管堆)都在进程的虚拟内存中。(在32位处理器上每个进程的虚拟内存为4GB) 堆栈stack 堆栈中存储值类型。 堆栈实际上是向下填充,即由高内存地址指向低内存地址...
  • lin37985
  • lin37985
  • 2015年06月20日 23:41
  • 734

线程堆栈(Thread Stack)和托管堆(Managed Heap)

内存格局通常分为四个区 全局数据区:存放全局变量,静态数据,常量 代码区:存放所有的程序代码 栈区:存放为运行而分配的局部变量,参数,返回数据,返回地址等, 堆区:即自由存储区       ...
  • xxdddail
  • xxdddail
  • 2014年07月04日 17:23
  • 2045

堆(heap)和栈(stack)有什么区别??

堆栈存放用户自己用malloc分配的空间,用free来释放;由用户自己管理,而栈存放函数的局部变量,由编译器来负责分配和回收管理,速度快,但容量有限, 简单的可以理解为:  heap:是由ma...
  • ly0303521
  • ly0303521
  • 2015年04月09日 19:40
  • 1661

托管堆与堆栈

内存格局通常分为四个区   全局数据区:存放全局变量,静态数据,常量   代码区:存放所有的程序代码   栈区:存放为运行而分配的局部变量,参数,返回数据,返回地址等,   堆区:即自由存储区...
  • Lyncai
  • Lyncai
  • 2013年07月31日 10:42
  • 2100

C# 托管资源 与 非托管资源

C# 托管资源 与 非托管资源托管资源一般是指被CLR控制的内存资源,这些资源的管理可以由CLR来控制,.NET可以自动进行回收,主要是指托管堆上分配的内存资源。例如程序中分配的对象,作用域内的变量等...
  • bingkxin
  • bingkxin
  • 2016年03月17日 11:33
  • 973

数据结构-堆(heap)

堆(heap)也被称为优先队列(priority queue)。队列中允许的操作是先进先出(FIFO),在队尾插入元素,在队头取出元素。而堆也是一样,在堆底插入元素,在堆顶取出元素,但是堆中元素的排列...
  • juanqinyang
  • juanqinyang
  • 2016年05月15日 20:00
  • 1526

数据结构之堆(Heap)及其用途

图、码、文 介绍 优先队列之堆 优先队列;最大树;最大堆、最小堆的插入、删除、初始化; 堆的用途:堆排序;haffman编码;haffman tree 解决优化问题...
  • a1459268562
  • a1459268562
  • 2016年12月07日 18:57
  • 251
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深入.NET托管堆(managed heap)(上)
举报原因:
原因补充:

(最多只允许输入30个字)