自动内存管理(垃圾回收)

1、理解垃圾回收平台的基本工作原理

  在面向对象的环境中,每个类型都代表可供程序使用的一种资源,使用资源,需要分配内存。如何访问资源?

  •   调用IL指令newobj,为代表资源的类型分配内存。在C#中使用new操作符,编译器就会自动生成该指令。

    CLR执行了以下操作(托管堆分配资源):

    1、计算类型(及其所有基类型)的字段需要的字节数

    2、加上对象的开销所需要的字节数(类型对象指针,同步块索引)两个字段占用的空间都是一样的,如果是32位的,则32位,即8字节。64的,16字节。

    3、CLR检查保留区域是否能够提供分配对象所需的字节数,如有必要就提交存储(将存储空间“交”给预订者)。如果托管堆有足够的可用空间,对象会被放       入。注意对象是在NextObjPtr指针(托管堆维护的一个指针,指向下一个对象在队中的分配位置),并且为它分配的字节会被清零。接着,调用类型的实例构造器(为this参数产地NextObjPtr),IL指令newobj将返回对象的地址。就在地址返回之前,NextObjPtr指针的值会加上对象占据的字节数,这样会得到一个新值,它指向下一个对象放入托管堆时的地址。

  •   初始化内存,设置资源的初始状态,使资源可用。类型的实例构造器扶着设置该初始状态。
  •   访问类型的成员来使用资源
  •   摧毁资源的状态以进行清理。(如:Finalize,Dispose,Close)
  •   释放内存,垃圾回收器独自负责这一步

  而C#中有一个机制为开发人员简化对内存管理任务——垃圾回收

  应用程序调用new操作符创建对象时,可能没有足够的地址空间来分配该对象。托管堆将对象需要的字节数加到NextObjPrt指针中的地址上来检测这种情况。如果结果值超过了地址空间的末尾,表示托管堆已满,必须执行一次垃圾回收。

写个程序看看效果

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Timer t = new Timer(TimerCallback, null, 0, 2000);

            Console.Read();
        }

        // 回调方法
        private static void TimerCallback(object o)
        {
            //回调时候,显示日期
            Console.WriteLine("In TimerCallback :"+DateTime.Now.ToString());
            // 垃圾回收
            GC.Collect();

        }
    }
}

在上述代码中,TimerCallback方法只被调用了一次!!

分析:

因为回调函数TimerCallback中,通过调用GC.Collect()强制垃圾回收一次。回收开始时,垃圾回收器首先假定堆中所有对象都是不可达的(垃圾);eg:Timer对象。然后,垃圾回收器检查应用程序的根,发现在初始化后,Main方法没有再调用变量t.对象不可达,回收对象t的内存,所以TimerCallback只被调用了一次。

而在调试模式下,“监视”窗口查看t引用对象,因为对象已经被回收,所以调试器无法显示该对象。

当然微软给了一个解决方案。

JIT编译器将方法的IL代码编译成本地代码时,JIT编译器会检查两点:定义方法的程序集在编译时有没有优化;进程当前在一个调试器中执行。如果这两点都城里,JIT编译器在生成方法的内部根表时,会将所有变量的生存期手动延长至方法结束。换言之,JIT编译器自己骗自己,让自己认为Main中的t变量必须生存到方法结束。所以,如果发生了垃圾回收,垃圾回收器会认为t仍然是一个根,t引用的Timer对象仍然可达。Timer对象会在回收中存活,TimerCallback方法会被重复调用,直至Console.Read()方法返回而且Main()方法退出。

同理,命令行中重新编译程序,但是制定C#编译器的/debug+编译器开关。运行执行文件,仍会发现TimerCallback多次调用。这是因为当JIT编译方法时,JIT编译器会检查定义方法的程序集是否应用了System.Diagnostics.DebuggableAttribute,而且它的DebugingModes的DisableOptimizations标志是否被设置。只要JIT编译器发现该编制已设置,同样会编译方法时将所有变量的生存期手动延长到方法结束。指定/debug+编译器开关后,C#编译器会在最终的程序集中自动生成该attribute和标志。注意,C#编译器的/optimize+编译器开关会将DisableOptimizations禁止的优化重新恢复,所以做这个测试的时候,不要打开这个开关。

 

  static void Main(string[] args)
        {
            Timer t = new Timer(TimerCallback, null, 0, 2000);

            Console.Read();

            t = null;
        }

但是,如果编译器(不使用/debug+编译器开关),TimerCallback仍只调用了一次。这里的问题在于,JIT编译器是一个优化编译器,将局部变量或者参数变量设为null,等价于根本不引用该变量。相当于,JIT编译器,会将 t=null;这段代码优化掉;

  static void Main(string[] args)
        {
            Timer t = new Timer(TimerCallback, null, 0, 2000);

            Console.Read();

            t.Dispose();
        }

这样TimerCallback正确的重复调用,现在只有t存活,才会调用Dispose()方法,变量t可到达。

我想有时候项目里的用Debug编译出了问题,就在这里???

 如果我们在同一个方法中,创造过多的引用对象,这样会对程序的性能造成影响,因此我们要遵守以下的规则,减少GC的工作量

1、将常用的局部变量提升为成员变量

所有的引用类型,包括那些局部变量,都会分配到堆上。在函数退出后,函数内的所有局部变量都会立即变成垃圾对象。所以我们可以得出结论:

若是某个引用类型(值类型无所谓)的局部变量用于被频繁调用的例程中,那么应该将其提升为成员变量。这既有助于减轻GC的负担,也可以提升程序运行的效率。

代码如下

  protected override void OnPaint(PaintEventArgs e)
  {
     using (Font myFont = new Font("Arial", 10.0f))
     {
          e.Graphics.DrawString(DateTime.Now.ToString(), myFont, Brushes.Black, new Point(0, 0));
     }
     base.OnPaint(e);
   }

将会被非常频繁的调用,每次调用都会创建一个Font对象,而包含的内容完全和上一次一样。所以GC需要每次都为你清扫这些垃圾,严重影响了应用程序的效率。其实我们完全可以将Font对象提升为成员变量,是每次窗体重绘时能够重用该Font对象:

 Font myFont = new Font("Arial", 10.0f)

 protected override void OnPaint(PaintEventArgs e)
  {
     e.Graphics.DrawString(DateTime.Now.ToString(), myFont, Brushes.Black, new Point(0, 0));
    
     base.OnPaint(e);
   }

2、为常用的类型实例提供静态对象

静态成员变量可以让引用类型在类的各个实例中共享。我们可以通过提供了一个类,存放某个类型常用的实例的单例对象,这样可以避免创建重复的对象。.NET Framework 的类库中就有很多这样的做法:Brush 类包含了一系列的静态Brush对象,每个都包含了一种常用的颜色。它们的简要实现如下:

    private static Brush blackBrush;
    public static Brush Black
    {
    get
       {
        if (blackBrush == null)
            blackBrush = new SolidBrush(Color.Black);
           return blackBrush;
        }
     }

 

3.为不可变类型提供可变的创建对象

 System.String类型时一个不可变类型:即在构造一个字符串对象后,其内容不能被修改。如果对一个字符串进行修改时,实际上时创建了一个新的字符串对象,从前的字符串对象也就变成了垃圾。看下面的代码:

string smg="how";
smg+="do";
smg+="you";
smg+="do"

string类型的+=操作符会创建一个新的字符串对象并返回,对于这类拼接字符串的工作应该交给更适合的string.Format()方法:

string msg = string.Format("{0} {1} {2}{3}", "How", "do", "you", "do");

如果需要进行一些比简单拼接字符串更加复杂的工作,可以考虑使用StringBuilder类,该类是一个可变的字符串,用来创建不可变的string对象。对于经常变化的stirng对象,使用StringBuilder对象来替换是一个非常好的选择 —— 当我们的某个设计需要不可变类型时,应该考虑提供一个创建对象,专门负责分步地构造出最终对象(例如:string对象之于StringBuilder对象)。这样既可让用户分步地创建出最终对象,也可以保证对象的不可变性。

 

 

 

 

posted @ 2012-11-28 22:46  秦小米  Views(216)  Comments(0Edit  收藏  举报