`
北极的。鱼
  • 浏览: 159043 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

深入.NET托管堆(managed heap)

 
阅读更多

转自http://blog.sina.com.cn/s/blog_538342930100nlmc.html

 

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

 

为什么要托管堆?

 

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

 

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

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

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

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

● 整理堆以减少碎片

 

托管堆优化

 

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

 

托管堆的另外一种优化操作与locality of reference规则有关。该规则表明,一起分配的对象经常被一起使用。如果对象们在堆中位置很紧凑的话,高速缓存的性能将会得到提高。由于托管堆的天性,对象们总是被分配在连续的地址上,托管堆总是保持紧凑,结果使得对象们始终彼此靠近,永远不会分得很远。这一点与标准堆提供的非托管代码形成了鲜明的对比,在标准堆中,堆很容易变成碎片,而且一起分配的对象经常分得很远。根据本人查看的一些资料,表明,在C++中堆是类似链表,所以会出现碎片。而.net的CLR组建的堆类似数组,没有碎片,所以效率很高。

 

还有一种优化是与大对象有关的。通常,大对象具有很长的生存期。当一个大对象在.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 0和generation 1中执行GC过程。如同在generation 0中一样,不再使用的对象被释放,正在被使用的对象被整理并移入下一个generation中。大部分GC过程的主要目标是generation 0,因为在generation 0中最有可能存在大量的已不再使用的临时对象。对generation 2的回收过程具有很高的开销,并且此过程只有在generation 0和generation 1的GC过程不能释放足够的内存时才会被触发。如果对generation 2的GC过程仍然不能释放足够的内存,那么系统就会抛出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

    }

}

在这里,对象可以通过两种方式被清除。第一种方式是通过IDisposable接口的Dispose方法。此方法在对象显式地结束时被客户代码调用,它调用InternalDispose(true)。在这种情况下所有的对象都被清除了。如果析构函数被调用,那么InternalDispose(false)被调用,此时只有外部资源会被释放。如果我们已经执行了终止操作,那么我们自己的对象有可能已经被释放了,此后对它们的引用有可能引起异常。

 

对GC.SuppressFinalize的调用会阻止垃圾收集器将对象放入终止队列中。这样做可以降低在一次GC过程中由于整理对象而引起的内存消耗,并且由于终止操作不会被调用,从而使性能得到提高。Requests that the common language runtime not call the finalizer for the specified object. (Quoted from MSDN)

 

对C#的优化

 

因此使用IDisposable.Dispose()来释放资源是个很好的方式,它不但可以减少一些在托管堆上进行操作的内存需求,而且能够减少必须执行终止操作的对象的数量。但是它使用起来比较麻烦,尤其是有多个临时对象被创建的时候更是如此。为了能够从IDisposable接口受益,C#客户程序应该书写象下面这样的代码:

 

OverdueBookLocator bookLocator = null;

try

{

    bookLocator = new OverdueBookLocator();

    // Use bookLocator here

    Book book = bookLocator.Find("Eiffel, the Language");

    .

    .

    .

}

finally

{

    if(bookLocator != null)

    {

        IDisposable disp = bookLocator as IDisposable;

        disp.Dispose();

    }

}

 

finally中的代码被用来在有异常发生时作适当的清理工作。为了C#客户程序能够简单有效地使用Dispose模式,Beta2引入了using表达式。Using表达式允许你简化你的代码,因此上面的代码可以写成:

 

 

using(bookLocator = new OverdueBookLocator())

{

   // Use bookLocator here

   Book book = bookLocator.Find("Eiffel, the Language");

}

 

无论何时分配具有明确定义的生存期的类型时,你都应该使用using表达式。它能保证对IDisposable接口的适当调用,即使是在有异常发生的时候。

 

使用System.GC类

 

System.GC类用来访问被.NET framework暴露出来的垃圾回收机制。这个类包含以下一些有用的方法:

 

●     GC.SuppressFinalize 这个方法在前面已经描述过了,它能够抑制终止操作。如果你已经将属于一个对象的外部资源释放了,调用这个方法来抑制此对象的终止操作的执行。

●     GC.Collect 具有两个版本。不带参数的版本在托管堆的所有generation上执行回收动作。另一个版本带有一个整型参数,此参数指明所要进行回收操作的generation。你将很少调用这个方法,因为垃圾收集器在需要的时候会自动调用它。

●     GC.GetGeneration 返回作为参数传入的对象所在的generation。这个方法在由于性能的原因而进行的调试和跟踪中很有作用,但是在大部分应用中作用有限。

●     GC.GetTotalMemory 返回堆中已经被分配的内存总量。由于托管堆的工作方式,这个数字并不精确,但是如果你以true作为参数的话,还是会得到一个比较接近的近似值。这个方法在计算之前会先执行一遍回收操作。

 

下面是使用这些方法的一个例子:

 

/// <summary>

/// Displays current GC information

/// </summary>

/// <param name="generation">The generation to collect</param>

/// <param name="waitForGC">Run GC before calculating usage?</param>

public void CollectAndAudit(int generation, bool waitForGC)

{

  int myGeneration = GC.GetGeneration(this);

  long totalMemory = GC.GetTotalMemory(waitForGC);

  Console.WriteLine("I am in generation {0}.", myGeneration);

  Console.WriteLine("Memory before collection {0}.", totalMemory);

  GC.Collect(generation);

  Console.WriteLine("Memory after collection {0}.", totalMemory);

}

 

关于本文作者

 

Mickey Williams是Codev Technologies的创始人之一。Codev Technologies是一家从事位Windows程序开发者提供咨询和工具的机构。他同时也是.NET Experts (http://www.codeguru.com/columns/DotNet/www.dotnetexperts.com)的主要成员,他在此讲授.NET Framework的课程。他时常在美国和欧洲的一些研讨会上发表演讲,并且已经写了八本有关Windows程序设计方面的著作。他目前正被微软出版社邀请写作“Microsoft Visual C#”。你可以在mw@codevtech.com找到他。

分享到:
评论

相关推荐

    C#的内存管理:堆、栈、托管堆与指针

    托管堆(Managed Heap)是 .NET Framework 的一部分,用于存储引用类型数据,如类、对象等。托管堆的出现是 .NET Framework 与其他语言的不同之处。托管堆受垃圾收集器的控制和管理,当对象不再使用时,垃圾收集器将...

    堆栈和托管堆的详解堆栈和托管堆的详解

    托管堆(Managed Heap)是.NET框架中的一个重要组成部分,专门用于存储引用类型的对象。与堆栈不同,托管堆的内存管理由.NET运行时环境自动处理,包括对象的创建、存储以及垃圾回收等操作。 **特点:** 1. **动态...

    C#内存管理简介在C#中,虚拟内存中有个两个存储变量的区域,一个称为堆栈,一个称为托管堆

    2. **托管堆(Managed Heap)**:托管堆是.NET框架提供的一种特殊内存区域,专门用于存储引用类型,如类实例、对象等。当创建一个对象时,其实例数据会被分配到托管堆上。与堆栈不同,托管堆的内存管理是由垃圾收集...

    垃圾回收:在微软.NET框架自动内存管理

    首先,需要明确的是,.NET框架通过托管堆(Managed Heap)来管理内存资源。所有通过.NET编程语言创建的对象都是托管对象,并且这些对象在内存中存储于托管堆上。托管堆与传统的C运行时堆(C Runtime Heap)不同,...

    高清彩版 Apress.Pro.NET.Memory.Management

    在.NET框架中,内存管理主要通过托管堆(Managed Heap)和垃圾回收器(Garbage Collector, GC)来实现。托管堆是.NET运行时环境为应用程序分配的一块连续的内存区域,用于存储对象实例。当应用程序创建新对象时,...

    .net 程序减少系统内存

    ### 托管堆(Managed Heap) 托管堆是.NET运行时自动管理的一块内存区域,用于存储所有由.NET语言创建的对象。当一个对象不再被引用时,CLR的垃圾回收器会自动回收该对象所占用的内存,从而避免了内存泄漏的问题。...

    .NET垃圾回收器(GC)原理浅析

    托管堆(Managed Heap) 先来看MSDN的解释:初始化新进程时,运行时会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。 “托管堆也是堆”,为什么这样说呢?这么说是希望大家不要被“术语”迷惑,...

    理解C#对象生命周期

    对象是在一个叫做托管堆(managedheap)的内存中分配的,它们会被垃圾回收器(garbage collector)自动销毁。在讲解之前,你必须知道类(class),对象(object),引用(reference),栈(stack)和堆(heap)的意思。一个类...

    浅析C#编程中的内存管理

    首先,C#中的内存主要分为托管堆(Managed Heap)和非托管堆(Unmanaged Heap)。托管堆是由.NET Framework管理的,用于存储对象实例,而非托管堆则主要用于存储如原生类型(如指针)和系统级资源(如文件句柄)。C#...

    谈谈.net对象生命周期(垃圾回收)

    当创建一个.NET对象时,它会被分配到托管堆(Managed Heap)上。托管堆是由.NET的公共语言运行库(Common Language Runtime, CLR)管理的,这意味着程序员无需关心对象的内存分配和释放。对象的引用则会被保存在栈...

    c#监测内存使用大小

    首先,我们要知道C#中的内存主要分为两大部分:托管堆(Managed Heap)和非托管堆(Unmanaged Heap)。托管堆由.NET框架的垃圾回收器(Garbage Collector, GC)管理,用于存储对象实例。非托管堆则包含操作系统分配...

    CLR via C# 4th edition.pdf

    此外,书还涵盖了垃圾回收器(garbage collector)的工作原理,以及如何利用CLR的托管堆(managed heap)。 Jeffrey Richter通过本书还教授读者如何快速开始使用序列化和反序列化服务,设计响应式、可扩展的解决...

    GC垃圾回收机制

    一个托管堆(managed heap)是垃圾回收器从操作系统申请的内存区(通过调用 windows api VirtualAlloc)。当 CLR 载入内存之后,会初始化两个托管堆,一个大对象堆(LOH –large object heap)和一个小对象对(SOH ...

    Java与C#的垃圾回收机制

    - **托管堆(Managed Heap)**:存放所有托管对象,是C#中垃圾回收的主要场所。 - **非托管堆(Unmanaged Heap)**:由程序员直接控制分配与释放。 - **方法区(Method Area)**:类似于Java中的方法区,存储类型信息。 - ...

    java C#垃圾回收算法分析

    - **托管堆(Managed Heap)**:与Java不同,C#的堆分为小对象堆和大对象堆。小对象堆主要存放小对象,使用复制算法;大对象堆存放大对象,使用标记-清除算法。 - **并行和并发GC**:.NET支持并行GC,即多个CPU核心...

    NET调试:转储正在运行的托管代码过程中的所有字符串

    高级调试过程往往需要深入理解.NET框架的内部工作原理,包括CLR(Common Language Runtime)如何管理内存,以及如何利用调试工具提供的信息来定位问题。同时,对于大型项目或复杂系统,理解代码架构和设计模式也非常...

Global site tag (gtag.js) - Google Analytics