C#垃圾回收之二次回收机制

来源:互联网 发布:c语言入门自学 编辑:程序博客网 时间:2024/04/30 03:29
    MSDN2原文:
    实现 Finalize 方法或析构函数对性能可能会有负面影响,因此应避免不必要地使用它们。用 Finalize方法回收对象使用的内存需要至少两次垃圾回收。当垃圾回收器执行回收时,它只回收没有终结器的不可访问对象的内存。这时,它不能回收具有终结器的不可访问对象。它改为将这些对象的项从终止队列中移除并将它们放置在标为准备终止的对象列表中。该列表中的项指向托管堆中准备被调用其终止代码的对象。垃圾回收器为此列表中的对象调用Finalize方法,然后,将这些项从列表中移除。后来的垃圾回收将确定终止的对象确实是垃圾,因为标为准备终止对象的列表中的项不再指向它们。在后来的垃圾回收中,实际上回收了对象的内存。
 

    如果对象所在的类没有实现Finalize方法,或者对象所在的类没有定义析构函数(实际上析构函数隐式的转化为Finalize方法了,见之前的文章),则调用GC.Collect()时,如果该对象是不可访问的,则此对象一定就会被回收了。
    如果对象是不可访问的,而且该对象所在的类定义了Finalize方法,则调用GC.Collect()时,会将该对象从待终结队列中移动到另外一个队列(指针指向),该队列中的对象都是等待调用Finalize方法的。
    从上面的图片和论述中,我们可以很清楚没有实现Finalize方法的类的对象如何收回它的内存,以及什么时候可以手动收回,我们都清清楚楚。关键是实现了Finalize方法的类的对象呢?GC.Collect()把它放到待调用Finalize()方法队列后,又是何时去调用Finalize()方法的呢?
    以下讨论的都是实现了Finalize()方法的情况,即二次回收的情况,不讨论没有实现Finalize()方法的情况。   
    一、如果对象是可访问的,就算显示调用GC.Collect(),并且紧接着就调用
GC.WaitForPendingFinalizers(),该对象的Finalize方法也不会被调到。
    上代码:
  1. using System;
  2. using System.Threading;
  3. using System.IO;
  4. using System.Data.SqlClient;
  5. using System.Net;

  6. namespace Lab
  7. {
  8.     class Log
  9.     {
  10.         public static void Write(string s)
  11.         {
  12.             Console.WriteLine("{0}/tTotalMilliseconds:{1}/tTotalMemory:{2}",s,DateTime.Now.TimeOfDay.TotalMilliseconds,GC.GetTotalMemory(false));
  13.         }
  14.     }

  15.     class World
  16.     {
  17.         protected SqlConnection conn = null;

  18.         public World()
  19.         {
  20.             conn = new SqlConnection();
  21.         }

  22.         protected void Finalize()
  23.         {
  24.             conn.Dispose();
  25.             Log.Write("World's destructor is called");
  26.         }
  27.     }

  28.     class China : World
  29.     {
  30.         public China()
  31.             : base()
  32.         {

  33.         }

  34.         ~China()
  35.         {
  36.             Log.Write("China's destructor is called");
  37.         }

  38.     }

  39.     class Beijing : China
  40.     {
  41.         public Beijing()
  42.             : base()
  43.         {

  44.         }

  45.         ~Beijing()
  46.         {
  47.             Log.Write("Beijing's destructor is called");
  48.         }
  49.     }
  50. }

  51. namespace Lab
  52. {
  53.     class Program
  54.     {
  55.         static void Main(string[] args)
  56.         {
  57.             TestOne();
  58.             Log.Write("进入Main/t/t");
  59.             Console.ReadLine();
  60.         }

  61.         static void TestOne()
  62.         {
  63.             Log.Write("对象创建之前/t/t");
  64.             Beijing bj = new Beijing();
  65.             Log.Write("对象创建之后,垃圾回收之前/t/t");
  66.             GC.Collect();
  67.             GC.WaitForPendingFinalizers();//此方法相当于join终结器线程,等待执行完毕。
  68.             Log.Write("垃圾回收之后/t/t");
  69.         }
  70.     }
  71. }
运行结果:

    实际上,上面的代码只运行到了Main()里的
Console.ReadLine();这一行还没有跑,如果我们在控制台再随便录入什么东西,然后回车,就会发现Beijing的析构函数被调用了,紧接着China的析构函数也被调用了,再接着World的Finalize()也被调用了,而不是在GC.WaitForPendingFinalizers()方法中就调用这些Finalize()方法,为什么会这样呢?
    MSDN2上有说明:
    在应用程序域(注意:不是作用域哦)的关闭过程中,对没有免除终结的对象将自动调用Finalize,即使那些对象仍是可访问的。
    在Main方法关闭时,即应用程序域退出的时候,按照上面的说法bj这个对象的Finalize()方法被调用,它的finalize()的调用触发了它爸爸,以及它爷爷的finalize()方法的调用。
    从结果中也可以看出来,
在一个对象还可以访问时,调用GC.Collect()以及GC.WaitForPendingFinalizers()都不会收回该对象的内存(不会触发该对象的finalize()方法的执行)。
    二、如果对象已不可访问,那此时调用GC.Collect()以及GC.WaitForPendingFinalizers(),该对象的内存会被回收(前提是它的finalize方法中不要再让它复活了)
  1. using System;
  2. using System.Threading;
  3. using System.IO;
  4. using System.Data.SqlClient;
  5. using System.Net;

  6. namespace Lab
  7. {
  8.     class Log
  9.     {
  10.         public static void Write(string s)
  11.         {
  12.             Console.WriteLine("{0}/tTotalMilliseconds:{1}/tTotalMemory:{2}",s,DateTime.Now.TimeOfDay.TotalMilliseconds,GC.GetTotalMemory(false));
  13.         }
  14.     }

  15.     class World
  16.     {
  17.         protected SqlConnection conn = null;

  18.         public World()
  19.         {
  20.             conn = new SqlConnection();
  21.         }

  22.         protected void Finalize()
  23.         {
  24.             conn.Dispose();
  25.             Log.Write("World's destructor is called");
  26.         }
  27.     }

  28.     class China : World
  29.     {
  30.         public China()
  31.             : base()
  32.         {

  33.         }

  34.         ~China()
  35.         {
  36.             Log.Write("China's destructor is called");
  37.         }

  38.     }

  39.     class Beijing : China
  40.     {
  41.         public Beijing()
  42.             : base()
  43.         {

  44.         }

  45.         ~Beijing()
  46.         {
  47.             Log.Write("Beijing's destructor is called");
  48.         }
  49.     }
  50. }

  51. namespace Lab
  52. {
  53.     class Program
  54.     {
  55.         static void Main(string[] args)
  56.         {
  57.             TestOne();
  58.             Log.Write("进入Main/t/t");
  59.             Console.ReadLine();
  60.         }

  61.         static void TestOne()
  62.         {
  63.             Log.Write("对象创建之前/t/t");
  64.             Beijing bj = new Beijing();
  65.             bj = null;   //此时new Beijing()所创建的内存已不可访问
  66.             Log.Write("对象创建之后,垃圾回收之前/t/t");
  67.             GC.Collect();
  68.             GC.WaitForPendingFinalizers();//此方法相当于join终结器线程,等待执行完毕。
  69.             Log.Write("垃圾回收之后/t/t");
  70.         }
  71.     }
  72. }

 结果如下图所示:
   

 当bj已不可访问时,此时调用GC.Collection(),再紧接着调用GC.WaitForPendingFinalizers(),则bj的finalize()方法,以及它爸爸的finalize方法、它爷爷的finalize方法都被调用了。
    三、有时候大家会发现对象的Finalize()方法被执行后,本来收集了垃圾,内存占用应该会减少,但紧接着调用GC.GetTotalMemory(false)的时候发现内存占用反而升了,这是为什么呢?因为托管堆中内存地址是连续分配的,收集了垃圾的话,就要挪动它后面的往前移,这是需要很大代价的,所以MSDN上说实现finalize()方法会对性能有所影响,而且finalize方法写的不好的话,比如让一个不可访问的对象又可以访问了,或者在结束时的忘记了调用Base.Finalize(),都会有负面影响。