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的工作量
所有的引用类型,包括那些局部变量,都会分配到堆上。在函数退出后,函数内的所有局部变量都会立即变成垃圾对象。所以我们可以得出结论:
若是某个引用类型(值类型无所谓)的局部变量用于被频繁调用的例程中,那么应该将其提升为成员变量。这既有助于减轻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对象)。这样既可让用户分步地创建出最终对象,也可以保证对象的不可变性。