详谈.net中的垃圾回收机制

1. 自动内存管理和GC
  在原始程序中堆的内存分配是这样的:找到第一个有足够空间的内存地址(没被占用的),然后将该内存分配。当程序不再需要此内存中的信息时程序员需要手动将此内存释放。堆的内存是公用的,也就是说所有进程都有可能覆盖另一进程的内存内容,这就是为什么很多设计不当的程序甚至会让操作系统本身都down掉。我们有时碰到的程序莫名其妙的死掉了(随机现象),也是因为内存管理不当引起的(可能由于本身程序的内存问题或是外来程序造成的)。另一个常见的实例就是大家经常看到的游戏的Trainer,他们通过直接修改游戏的内存达到"无敌"的效果。明白了这些我们可以想象如果内存地址被用混乱了的话会多么危险,我们也可以想象为什么C++程序员(某些)一提起指针就头疼的原因了。另外,如果程序中的内存不被程序员手动释放的话那么这个内存就不会被重新分配,直到电脑重起为止,也就是我们所说的内存泄漏。所说的这些是在非托管代码中,CLR通过AppDomain实现代码间的隔离避免了这些内存管理问题,也就是说一个AppDomain在一般情况下不能读/写另一AppDomain的内存。托管内存释放就由GC(Garbage Collector)来负责。我们要进一步讲述的就是这个GC,但是在这之前要先讲一下托管代码中内存的分配,托管堆中内存的分配是顺序的,也就是说一个挨着一个的分配。这样内存分配的速度就要比原始程序高,但是高出的速度会被GC找回去。为什么?看过GC的工作方式后你就会知道答案了。
2. GC工作方式
  首先我们要知道托管代码中的对象什么时候回收我们管不了(除非用GC.Collect**GC回收,这不推荐,后面会说明为什么)。GC会在它"高兴"的时候执行一次回收(这有许多原因,比如内存不够用时。这样做是为了提高内存分配、回收的效率)。那么如果我们用Destructor呢?同样不行,因为.NET中Destructor的概念已经不存在了,它变成了Finalizer,这会在后面讲到。目前请记住一个对象只有在没有任何引用的情况下才能够被回收。为了说明这一点请看下面这一段代码:


代码如下:

view sourceprint?object objA = new object(); 
object objB = objA; 
objA = null; 
// **回收。 
GC.Collect(); 
objB.ToString();

  这里objA引用的对象并没有被回收,因为这个对象还有另一个引用,ObjB。对象在没有任何引用后就有条件被回收了。
  当GC回收时,它会做以下几步:
  1、确定对象没有任何引用。
  2、检查对象是否在Finalizer表上有记录。如果在Finalizer表上有记录,那么将记录移到另外的一张表上,在这里我们叫它Finalizer2。如果不在Finalizer2表上有记录,那么释放内存。在Finalizer2表上的对象的Finalizer会在另外一个low priority的线程上执行后从表上删除。当对象被创建时GC会检查对象是否有Finalizer,如果有就会在Finalizer表中添加纪录。我们这里所说的记录其实就是指针。如果仔细看这几个步骤,我们就会发现有Finalizer的对象第一次不会被回收,也就是,有Finalizer的对象要一次以上的Collect操作才会被回收,这样就要慢一步,所以作者推荐除非是绝对需要不要创建Finalizer。
  GC为了提高回收的效率使用了Generation的概念,原理是这样的,第一次回收之前创建的对象属于Generation 0,之后,每次回收时这个Generation的号码就会向后挪一,也就是说,第二次回收时原来的Generation 0变成了Generation 1,而在第一次回收后和第二次回收前创建的对象将属于Generation 0。GC会先试着在属于Generation 0的对象中回收,因为这些是最新的,所以最有可能会被回收,比如一些函数中的局部变量在退出函数时就没有引用了(可被回收)。如果在Generation 0中回收了足够的内存,那么GC就不会再接着回收了,如果回收的还不够,那么GC就试着在Generation 1中回收,如果还不够就在Generation 2中回收,以此类推。Generation也有个最大限制,根据Framework版本而定,可以用GC.MaxGeneration获得。在回收了内存之后GC会重新排整内存,让数据间没有空格,这样是因为CLR顺序分配内存,所以内存之间不能有空着的内存。现在我们知道每次回收时都会浪费一定的CPU时间,这就是我说的一般不要手动GC.Collect的原因。
  当我们用Destructor的语法时,编译器会自动将它写为protected virtual void Finalize(),这个方法就是我所说的Finalizer。就象它的名字所说,它用来结束某些事物,不是用来摧毁(Destruct)事物。在Visual Basic中它就是以Finalize方法的形式出现的,所以Visual Basic程序员就不用操心了。C#程序员得用Destructor的语法写Finalizer,不过千万不要弄混了,.NET中已经没有Destructor了。C++中我们可以准确的知道什么时候会执行Destructor,不过在.NET中我们不能知道什么时候会执行Finalizer,因为它是在第一次对象回收操作后才执行的。我们也不能知道Finalizer的执行顺序,也就是说同样的情况下,A的Finalize可能先被执行,B的后执行,也可能A的后执行而B的先执行。也就是说,在Finalizer中我们的代码不能有任何的时间逻辑。下面我们以计算一个类有多少个实例为示例,指出Finalizer与Destructor的不同并指出在Finalizer中有时间逻辑的错误:


代码如下:

view sourceprint?public class CountObject { 
  public static int Count = 0; 
  public CountObject() { 
    Count++; 
  } 
  ~CountObject() { 
    Count--; 
  } 

static void Main() { 
  CountObject obj; 
  for (int i = 0; i < 5; i++) { 
    obj = null; // 这一步多余,这么写只是为了更清晰些! 
   obj = new CountObject(); 
  } 
  // Count不会是1,因为Finalizer不会马上被触发,要等到有一次回收操作后才会被触发。 
  Console.WriteLine(CountObject.Count); 
  Console.ReadLine(); 
}

  注意以上代码要是改用C++写的话会发生内存泄漏,因为我们没有用delete操作符手动清理内存,但是在托管代码中却不会发生内存泄漏,因为GC会自动检测没有引用了的对象并回收。这里作者推荐你只在实现IDisposable接口时配合使用Finalizer,在其他的情况下不要使用(可能会有特殊情况)。
3. 对象的复活
  什么?回收的对象也可以"复活"吗?没错,虽然这么说的定义不准确。让我们先来看一段代码:


代码如下:

view sourceprint?public class Resurrection { 
  public int Data; 
  public Resurrection(int data) { 
    this.Data = data; 
  } 
  ~Resurrection() { 
    Main.Instance = this; 
  } 

public class Main { 
  public static Resurrection Instance; 
  public static void Main() { 
    Instance = new Resurrection(1); 
    Instance = null; 
    GC.Collect(); 
    GC.WaitForPendingFinalizers(); 
    // 看到了吗,在这里“复活”了。 
    Console.WriteLine(Instance.Data); 
    Instance = null; 
    GC.Collect(); 
    Console.ReadLine(); 
  } 
}

  你可能会问:"既然这个对象能复活,那么这个对象在程序结束后会被回收吗?"。会,"为什么?"。让我们按照GC的工作方式走一遍你就明白是怎么回事了。
  1、执行Collect。检查引用。没问题,对象已经没有引用了。
  2、创建新实例时已经在Finalizer表上作了纪录,所以我们检查到了对象有Finalizer。
  3、因为查到了Finalizer,所以将记录移到Finalizer2表上。
  4、在Finalizer2表上有记录,所以不释放内存。
  5、Collect执行完毕。这时我们用了GC.WaitForPendingFinalizers,所以我们将等待所有Finalizer2表上的Finalizers的执行。
  6、Finalizer执行后我们的Instance就又引用了我们的对象。(复活了)
  7、再一次去除所有的引用。
  8、执行Collect。检查引用。没问题。
  9、由于上次已经将记录从Finalizer表删除,所以这次没有查到对象有Finalizer。
  10、在Finalizer2表上也不存在,所以对象的内存被释放了。
  非托管资源的释放到现在为止,我们说了托管内存的管理,那么当我们利用如数据库、文件等非托管资源时呢?这时我们就要用到.NET Framework中的标准:IDisposable接口。按照标准,所有有需要手动释放非托管资源的类都得实现此接口。这个接口只有一个方法,Dispose(),不过有相对的Guidelines指示如何实现此接口,在这里我向大家说一说。实现IDisposable这个接口的类需要有这样的结构:


代码如下:

view sourceprint?public class Base : IDisposable { 
  public void Dispose() { 
    this.Dispose(true); 
    GC.SupressFinalize(this); 
  } 
  protected virtual void Dispose(bool disposing) { 
    if (disposing) { 
      // 托管类 
    } 
    // 非托管资源释放 
  } 
  ~Base() { 
    this.Dispose(false); 
  } 

public class Derive : Base { 
  protected override void Dispose(bool disposing) { 
    if (disposing) { 
      // 托管类 
    } 
    // 非托管资源释放 
    base.Dispose(disposing); 
  } 
}

  为什么要这样设计呢?让我在后面解说一下。现在我们讲讲实现这个Dispose方法的几个准则:它不能扔出任何错误,重复的调用也不能扔出错误。也就是说,如果我已经调用了一个对象的Dispose,当我第二次调用Dispose的时候程序不应该出错,简单地说程序在第二次调用Dispose时不会做任何事。这些可以通过一个flag或多重if判断实现。一个对象的Dispose要做到释放这个对象的所有资源。拿一个继承类为例,继承类中用到了非托管资源所以它实现了IDisposable接口,如果继承类的基类也用到了非托管资源那么基类也得被释放,基类的资源如何在继承类中释放呢?当然是通过一个virtual/Overridable方法了,这样我们能保证每个Dispose都被调用到。这就是为什么我们的设计有一个virtual/Overridable的Dispose方法。注意我们首先要释放继承类的资源然后再释放基类的资源。因为非托管资源一定要被保障正确释放所以我们要定义一个Finalizer来避免程序员忘了调用Dispose的情况。上面的设计就采用了这种形式。如果我们手动调用Dispose方法就没有必要再保留Finalizer了,所以在Dispose中我们用了GC.SupressFinalize将对象从Finalizer表去掉,这样再回收时速度会更快。那么那个disposing和"托管类"是怎么回事呢?是这样:在"托管类"中写所有你想在调用Dispose时让其处于可释放状态的托管代码。还记得我们说过我们不知道托管代码是什么时候释放的吗?在这里我们只是去掉成员对象的引用让它处于可被回收状态,并不是直接释放内存。在"托管类"中这里我们也要写上所有实现了IDisposable的成员对象,因为他们也有Dispose,所以也需要在对象的Dispose中调用他们的Dispose,这样才能保证第二个准则。disposing是为了区分Dispose的调用方法,如果我们手动调用那么为了第二个准则"托管类"部分当然得执行,但如果是Finalizer调用的Dispose,这时候对象已经没有任何引用,也就是说对象的成员自然也就不存在了(无引用),也就没有必要执行"托管类"部分了,因为他们已经处于可被回收状态了。好了,这就是IDisposable接口的全部了。现在让我们来回想一下,以前我们可能认为有了Dispose内存就会马上被释放,这是错误的。只有非托管内存才会被马上释放,托管内存的释放由GC管理,我们不用管。
4. 弱引用的使用
   A = B,我们称这样的引用叫做强引用,GC就是通过检查强引用来决定一个对象是否是可以回收的。另外还有一种引用称作弱引用(WeakReference),这种引用不影响GC回收,这就是它的用处所在。你会问到底有什么用处。现在我们来假设我们有一个很胖的对象,也就是说它占用很多内存。我们用过了这个对象,打算将它的引用去掉好让GC可以回收内存,但是功夫不大我们又需要这个对象了,没办法,重新创建实例,怎么创建这么慢啊?有什么办法解决这样的问题?有,将对象留在内存中不就快了嘛!不过我们不想这样胖得对象总占着内存,而我们也不想总是创建这样胖的新实例,因为这样很耗时。那怎么办……?聪明的朋友一定已经猜到了我要说解决方法是弱引用。不错,就是它。我们可以创建一个这个胖对象的弱引用,这样在内存不够时GC可以回收,不影响内存使用,而在没有被GC回收前我们还可以再次利用该对象。这里有一个示例:


代码如下:

view sourceprint?public class Fat { 
  public int Data; 
  public Fat(int data) { 
    this.Data = data; 
  } 

public class Main { 
  public static void Main() { 
    Fat oFat = new Fat(1); 
    WeakReference oFatRef = new WeakReference(oFat); 
    // 从这里开始,Fat对象可以被回收了。 
    oFat = null; 
    if (oFatRef.IsAlive) { 
      Console.WriteLine(((Fat) oFatRef.Target).Data); // 1 
    } 
    // 强制回收。 
    GC.Collect(); 
    Console.WriteLine(oFatRef.IsAlive); // False 
    Console.ReadLine(); 
  } 
}

  这里我们的Fat其实并不是很胖,但是可以体现示例的本意:如何使用弱引用。那如果Fat有Finalizer呢,会怎样?如果Fat有Finalizer那么我们可能会用到WeakReference的另一个构造函数,当中有一参数叫做TrackResurrection,如果是True,只要Fat的内存没被释放我们就可以用它,也就是说Fat的Finalizer执行后我们还是可以恢复Fat(相当于第一次回收操作后还可恢复Fat);如果TrackResurrection是False,那么第一次回收操作后就不能恢复Fat对象了。
5. 总结
  我在这里写出了正篇文章的要点:
一个对象只当在没有任何引用的情况下才会被回收。
一个对象的内存不是马上释放的,GC会在任何时候将其回收。一般情况下不要强制回收工作。
如果没有特殊的需要不要写Finalizer。
不要在Finalizer中写一些有时间逻辑的代码。
在任何有非托管资源或含有Dispose的成员的类中实现IDisposable接口。
按照给出的Dispose设计写自己的Dispose代码。
当用胖对象时可以考虑弱引用的使用。

(0)

相关推荐

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

    作为.NET进阶内容的一部分,垃圾回收器(简称GC)是必须了解的内容.本着"通俗易懂"的原则,本文将解释CLR中垃圾回收器的工作原理. 基础知识 托管堆(Managed Heap) 先来看MSDN的解释:初始化新进程时,运行时会为进程保留一个连续的地址空间区域.这个保留的地址空间被称为托管堆. "托管堆也是堆",为什么这样说呢?这么说是希望大家不要被"术语"迷惑,这个知识点的前提是"值类型和引用类型的区别".这里假设读者已经知

  • .Net 垃圾回收机制详细介绍

    析构函数 析构函数不能有修饰符,如public.不能接受任何参数. 编译器自动将一个析构函数转换成对Object.Finalize方法的一个override版,如下. class Test { protected override void Finalize() { try {-} finally { base.Finalize(); } } } 垃圾回收器 .NET垃圾回收器会保证: l  每个对象都会被摧毁,它的析构函数一定会被运行.当一个程序结束后,所有对象都会被销毁. l  每个对象只被

  • .NET微信公众号 用户分组管理

    本文实例为大家分享了.NET微信用户分组管理代码,供大家参考,具体内容如下 Model层实体类: public class UserList { public string total { get; set; } public string count { get; set; } public userlistopenid data { get; set; } public string next_openid { get; set; } } public class userlistopeni

  • 详述ASP.Net中页面之间传参方法

    ASP.NET提供了卓越的事件驱动编程模型,让开发者简化了应用程序的总体设计,但是这个也造成了它固有的一些问题,例如,使用传统的ASP里,我们可以通过使用POST方法很容易地实现页面间传递值,同样的事情,在使用事件驱动编程模型的ASP.NET就不是那么容易了,当然了,我们仍然有一些方法可以实现同样的功能. 本文将试着使用不同的可能的方法来解决这个问题,但可以预见是,本文将包含使用querystring,session变量以及server.Transfer方法来实现页面间的值传递. 使用Query

  • .NET微信公众号客服接口

    本文实例为大家分享了微信公众号客服接口.NET代码,供大家参考,具体内容如下 Kf_account.cs代码: public partial class Kf_account : Form { private readonly DataTable adt_user = new DataTable(); private readonly string as_INIFile = Application.StartupPath + "\\user.ini"; public Kf_accoun

  • ASP.NET访问共享文件夹的详细步骤

    假设找找看的ASP.NET程序在A服务器,索引文件在B服务器的ZzkIndex共享文件夹中,访问地址是\\192.168.18.18\ZzkIndex\.要实现就是在A服务器的ASP.NET程序中能读写共享文件夹\\192.168.18.18\ZzkIndex\中的文件. 具体操作步骤: (注:A服务器为ASP.NET程序所在服务器,B服务器为共享文件夹所在服务器) ①在两台服务器上建立相同用户名.相同密码的Windows帐户(A与B服务器都要建),比如假设这里用户名是ZzkIndexer,密码

  • .NET微信公众号查看关注者接口

    本文实例为大家分享了java获取不同路径的方法,供大家参考,具体内容如下 实体类: public class userlist { public string total { get; set; } public string count { get; set; } public userlistopenid data { get; set; } public string next_openid { get; set; } } public class userlistopenid { pub

  • asp.net保存网上图片到服务器的实例

    本文讲述的是根据一个图片的url地址,保存图片到asp.net服务器端的实现方法. 建立GetImage.aspx页面,代码如下: <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="GetImage.aspx.cs" Inherits="KeleyiTestWeb.KImage.GetImage" %> <!DOCTYPE html PUBLI

  • ASP.NET中制作各种3D图表的方法

    前言 大家都知道通过图表控件,我们即能表示数据又能比较各种图表的数据,例如比较去年和今年的收入.图表的类型也有很多,如柱状图.折线图.条形图.组合图等等. 首先,我将展示如何绘制出一个简单的图表. 简单图表的步骤 步骤1 新建一个"ASP.NET Empty Web Site". 步骤2 在Solution Explorer 中添加新项目,添加new form. 步骤3 工具栏-标准-项目.我们会看到在.Net Framework Component.中有两个图表选项,打勾并保存..N

  • 详谈.net中的垃圾回收机制

    1. 自动内存管理和GC 在原始程序中堆的内存分配是这样的:找到第一个有足够空间的内存地址(没被占用的),然后将该内存分配.当程序不再需要此内存中的信息时程序员需要手动将此内存释放.堆的内存是公用的,也就是说所有进程都有可能覆盖另一进程的内存内容,这就是为什么很多设计不当的程序甚至会让操作系统本身都down掉.我们有时碰到的程序莫名其妙的死掉了(随机现象),也是因为内存管理不当引起的(可能由于本身程序的内存问题或是外来程序造成的).另一个常见的实例就是大家经常看到的游戏的Trainer,他们通过

  • 基于java中stack与heap的区别,java中的垃圾回收机制的相关介绍

    #. 在java中有两类内存.分别称为stack(堆栈)和heap(堆). stack是程序内存空间,因此所有的基本类型和对象的引用是存在stack中. heap是java虚拟机储存对象的,它是一个巨大的内存,当你创造一个对象,java虚拟机把对象放入heap中,把创造的对象的地址放入stack中. 因此,基本类型.对象的引用储存在stack中:对象储存在heap中. #. java中的垃圾回收机制 当你new一个新的对象,java分配必需的内存.当你用完一个对象时,java的垃圾回收器为你把内

  • C#开发中的垃圾回收机制简析

    GC的前世与今生 虽然本文是以.NET作为目标来讲述GC,但是GC的概念并非才诞生不久.早在1958年,由鼎鼎大名的图林奖得主John McCarthy所实现的Lisp语言就已经提供了GC的功能,这是GC的第一次出现.Lisp的程序员认为内存管理太重要了,所以不能由程序员自己来管理.但后来的日子里Lisp却没有成气候,采用内存手动管理的语言占据了上风,以C为代表.出于同样的理由,不同的人却又不同的看法,C程序员认为内存管理太重要了,所以不能由系统来管理,并且讥笑Lisp程序慢如乌龟的运行速度.的

  • 谈谈JavaScript中的垃圾回收机制

    JavaScript 具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存. 在编写 JavaScript 程序时,开发人员不用再关心内存使用问题,所需内存的分配以及无用内存的回收完全实现了自动管理. 这种垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存.为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间), 周期性地执行这一操作. 具体到浏览器中的实现,则通常有两个策略,分别为标记清除和引用计数. 一.标记清除 JavaScri

  • 解读PHP中的垃圾回收机制

    PHP的基本GC概念 PHP语言同其他语言一样,具有垃圾回收机制.那么今天我们要为大家讲解的内容就是关于PHP垃圾回收机制的相关问题.希望对大家有所帮助.PHP strtotime应用经验之谈PHP memory_get_usage()管理内存PHP unset全局变量运用问题详解PHP unset()函数销毁变量教你快速实现PHP全站权限验证一.PHP 垃圾回收机制(Garbage Collector 简称GC) 在PHP中,没有任何变量指向这个对象时,这个对象就成为垃圾.PHP会将其在内存中

  • php中session垃圾回收机制

    在PHP中,没有任何变量指向这个对象时,这个对象就成为垃圾.PHP会将其在内存中销毁:这是PHP的GC垃圾处理机制,防止内存溢出. GC的工作就是扫描所有的Session信息,用当前时间减去session最后修改的时间,同session.gc_maxlifetime参数进行比较,如果生存时间超过gc_maxlifetime(默认24分钟),就将该session删除. 当一个有效的请求发生时,PHP 会根据全局变量 session.gc_probability和session.gc_divisor

  • 一文带你回顾Java中的垃圾回收机制

    目录 介绍 重要条款: 使对象符合 GC 条件的方法 请求JVM运行垃圾收集器的方式 定稿 让我们举一个真实的例子,在那里我们使用垃圾收集器的概念. 现在获得正确的输出: 总结 介绍 在 C/C++ 中,程序员负责对象的创建和销毁.通常程序员会忽略无用对象的销毁.由于这种疏忽,在某些时候,为了创建新对象,可能没有足够的内存可用,整个程序将异常终止,导致OutOfMemoryErrors. 但是在 Java 中,程序员不需要关心所有不再使用的对象.垃圾回收机制自动销毁这些对象. 垃圾回收机制是守护

  • Java 中的垃圾回收机制详解

    目录 介绍 重要条款: 使对象符合 GC 条件的方法 请求JVM运行垃圾收集器的方式 定稿 总结 介绍 在 C/C++ 中,程序员负责对象的创建和销毁.通常程序员会忽略无用对象的销毁.由于这种疏忽,在某些时候,为了创建新对象,可能没有足够的内存可用,整个程序将异常终止,导致OutOfMemoryErrors. 但是在 Java 中,程序员不需要关心所有不再使用的对象.垃圾回收机制自动销毁这些对象. 垃圾回收机制是守护线程的最佳示例,因为它始终在后台运行. 垃圾回收机制的主要目标是通过销毁无法访问

  • 简单讲解Lua中的垃圾回收机制

    Lua使用基于被内置在Lua某些算法的垃圾收集自动内存管理.可以自动内存管理的结果,作为一个开发者: 没有必要担心的对象分配内存. 无需释放他们时,不再需要可将其设置为nil. Lua使用运行不时收集死的对象时,不再从Lua程序中访问垃圾收集器. 所有对象,包括表,用户数据,函数,线程,字符串等受自动内存管理. Lua使用增量标记和使用两个数字来控制其垃圾回收周期即垃圾收集暂停和垃圾收集器的步骤事半功倍清除收集器.这些值是在百分比和100的值是常等于1. 垃圾收集暂停 垃圾收集停顿被用于控制多长

  • PHP5.3的垃圾回收机制(动态存储分配方案)深入理解

    垃圾回收机制是一种动态存储分配方案.它会自动释放程序不再需要的已分配的内存块. 自动回收内存的过程叫垃圾收集.垃圾回收机制可以让程序员不必过分关心程序内存分配,从而将更多的精力投入到业务逻辑. 在现在的流行各种语言当中,垃圾回收机制是新一代语言所共有的特征,如Python.PHP.Eiffel.C#.Ruby等都使用了垃圾回收机制. 虽然垃圾回收是现在比较流行的做法,但是它的年纪已经不小了.早在20世纪60年代MIT开发的Lisp系统中就已经有了它的身影, 但是由于当时技术条件不成熟,从而使得垃

随机推荐