.NET中GC的存储结构及实现原理

GC(Garbage Collector,垃圾回收器)是一种自动回收内存的机制,释放已经不再使用的对象的内存空间。

  在.NET平台中,我们的托管代码一般都不再关心内存的管理,一切都有CLR(Common language Runtime)去帮我们完成了。当我们开辟内存空间用来创建对象时,使用new关键字,这时CLR会分配一块内存存放对象,大部分时候,我们都不用自己去释放内存空间,而是由CLR在某个适当的时候帮我们释放掉。 

  为什么要GC?

  1.创建新对象开辟内存空间,在使用完后需要释放内存,提高性能

  2.避免开发人员直接操作内存,提高安全性

  GC的过程

  我们运行.NET程序后,OS Loader首先识别出IL,然后会加载CLR的核心库,进行一系列的必要处理后,CLR来到我们编写的代码入口处执行。

  当我们的在代码中使用new操作符创建class时,CLR便在叫作GC堆(GC Heap)的内存区域上分配一块内存存放我们的对象,若对象的Size超过85K字节时,考虑到性能原因,将对象创建在LOH(Large Object Heap)上而不是GC堆上【注1】,若我们在class中定义了析构函数来释放非托管资源【注2】,则CLR会在一个叫做终结器队列(Finalizer Queue)的地方添加一个指向该class的项。

  我们的程序在运行的过程,在某个时候需要进行垃圾回收了【注3】,首先GC会暂时挂起所有线程,然后确定对象引用的roots【注4】,并根据引用关系创建出由roots出发可以达到的对象形成的对象图,这些对象暂时还在使用,而那些已创建的却不在对象图中的对象则是不可达到的,也就是垃圾了,属于要回收的对象。随后将仍然使用的对象移动到存活期更久的区域【注5】,更改区域指针以回收对象,压缩内存去除内存空隙,并修复对移动的仍存活对象的引用指针,对于有析构函数的对象,则第一次回收时不会回收,而是将其在终结器队列中移除,并添加到另一个标为准备终止的对象列表中,另一个GC线程会调用此列表指向的对象的Finalize(),回收非托管资源,然后将项从列表中移除,下一次的GC才会真正回收掉该对象。

  注1:对象创建在Heap上的细节

  1)为了更高效的进行GC,.NET将GC堆分成了3个代,Gen0,Gen1和Gen2。

  2)这3个代只是逻辑上的划分,在内存中,他们的地址是连续的。

  3)Gen0和Gen1之和的大小大约是16M(workstation GC模式下)和64M(server GC模式下)。

  4)新创建对象Size小于85k位于Gen0上,大于85K的则创建在LOH上。

  注2:定义析构函数释放非托管资源

  Finalize方法是用来释放对象中使用的非托管资源,他是作为Dispose()方法的一种安全防护措施,即代码中没有显示的调用Dispose()来释放非托管资源时,GC时调用Finalize方法来释放,Finalize方法中并不直接释放非托管资源,而是调用Dispose(false)来释放。自.NET2.0起,C#中不能直接override Finalize方法,是通过析构函数来实现,析构函数在IL中会被解释为:

protected   override   void  Finalize()
{     
  try     
  {         
    // 执行自定义资源清理操作     
  }     
  finally     
  {         
    base .Finalize();     
  }
}

  默认情况下,一个类是没有析构函数的,那么在GC时是不会调用其Finalize()方法的。

  注3:GC发生的时机

  0代和1代垃圾回收主要由阀值控制。初始时Gen 0 heap大小与CPU缓存的大小相关,运行时CLR根据内存请求状态动态调整Gen 0 heap大小,但Gen 0和Gen 1总大小保持在16M左右
  Gen 2 heap和LOH都在full GC时进行回收,full GC主要由2类事件触发:
      a). 进入Gen 2 heap和LOH的对象很多,超过了一定比例。RegisterForFullGCNotification的参数 maxGenerationThreshold、largeObjectHeapThreshold可以分别为Gen 2 heap和LOH设定这个值
      b). 操作系统内存吃紧的时候。CLR会接收到操作系统内存紧张的通知消息,触发full GC

   注4:确定对象引用根

  对象的引用根主要来自于:FInalize Queue,CPU寄存器中的对象指针,全局对象、静态变量、局部对象、函数调用参数等。

  注5: GC时对象的转移

  1)Gen0 GC时,会将Gen0中存活的对象整体移动到Gen1中,然后压缩Gen1,使Gen1中的内存连续,同理Gen1中移动到Gen2。

 

  2)Gen2 GC时,此时发生的GC也称为Full GC,会回收整个Heap上的对象,Gen2上的对象将不再移动,而是压缩内存空间。

  3)LOH中的对象在Full GC时被回收,但其内存不会被压缩,而是使用一个空闲列表free list记录LOH中的空闲空间,对释放出来的空间进行管理。

  4)若对象是pinned object,则此对象不能被移动, 会造成内存碎片。

 

为什么要托管堆?
 
.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 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过程中由于整理对象而引起的内存消耗,并且由于终止操作不会被调用,从而使性能得到提高。
 
对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);
}

 

转载于:https://www.cnblogs.com/leo7718/p/3756757.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值