今天抽空来讨论一下.Net的垃圾回收与内存治理机制,也算是完成上个《WCF分布式开发必备知识》系列后的一次休息吧。以前被别人口试的时候问过我GC工作原理的题目,我现在口试新人的时候偶然也会问相关的题目。那么你是否也碰到这样的题目呢?好比你清晰.Net的垃圾回收机制吗?你能简述一下GC的工作原理吗?怎么样才能有效的治理内存呢?Using语句体内实例化的对象有什么作用?等等相关题目。下面我们就来具体讨论一下。相信你看完以后也可以口试别人。
本节的组织如下,1..Net的类型和内存分配2.GC垃圾收集器的工作原理3.什么长短托管资源4.如何有效开释对象资源。总结.现在开始我们本节的学习。
1..Net的类型和内存分配
Net中的所有类型都是(直接或间接)从System.Object类型派生的。
CTS中的类型被分成两大类——引用类型(reference type,又叫托管类型[managed type]),分配在内存堆上,值类型(value type)。值类型分配在堆栈上。如图
值类型在栈里,提高前辈后出,值类型变量的生命有先后顺序,这个确保了值类型变量在推出作用域以前会开释资源。比引用类型更简朴和高效。堆栈是从高地址往低地址分配内存。
引用类型分配在托管堆(Managed Heap)上,声明一个变量在栈上保留,当使用new创建对象时,会把对象的地址存储在这个变量里。托管堆相反,从低地址往高地址分配内存,如图
2.GC垃圾收集器的工作原理
上图中,当dataSet使用过时以后,我们不显示销毁对象,堆上的对象还继承存在,等待GC的 回收。
垃圾收集器通过分代支持对象的春秋化是推荐的但不是必须的。一代在内存里是一个具有相对春秋的对象的单位。对象的
代号或春秋标识对象属于那个分代。在应用程序的生命周期里,越近创建的对象属于越新的代,并且比早创建的对象具有
较低的分代号。最近分代里的对象代号是0.
在new对象时,要先搜索空闲链表,找到最适合内存块,分配,调整内存块链表,合并碎片。new操纵几乎可以在O(1)的时间完成,把堆顶指针加1。工作原理是: 当托管堆上剩余空间不足,或者Generator 0 的空间已满的时候GC运行,开始回收内存。垃圾回收的开始,GC对堆内存的压缩调整,对象集中到顶部。GC在扫描垃圾的时候会占用一定的CPU时间片的,最初的GC算法真的是扫描整个堆,效率低。现在的GC把堆中的对象分成3代,最近进入堆的是第0代(generation 0), 其次是generation 1, generation2. 第一次GC只扫描第0代。假如回收的空间足够当前使用就不必扫描其它generation的对象。所以,GC创建对象的效率比C++高效,不需要扫描全部堆空间。它通过扫描策略,再加上内存治理策略带来的机能晋升,足以补偿GC所占用的CPU时间。
3.什么长短托管资源
常见的非托管资源就是包装操纵系统资源的对象,例如文件,窗口或网络连接,对于这类资源固然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但它知道如何清理这些资源。好在.net Framework提供的Finalize()方法,它答应在垃圾回收器回收该类资源前,适当的清理非托管资源。这里列举几种常见的非托管资源:画笔、流对象、组件对象等等资源(Object,ODBCDataReader,OleDBDataReader,Pen,Regex,Socket,StreamWriter,ApplicationContext,Brush,
Component,ComponentDesigner,Container,Context,Cursor,FileStream,
Font,Icon,Image,Matrix,Timer,Tooltip)。(参考MSDN)
4.如何有效开释非托管资源。
GC无法治理非托管资源,那么如何开释非托管资源呢?.Net提供了两种方式:
(1)析构函数:垃圾收集器回收非托管对象的资源时,会调用对象的终结方法Finalize(),进行资源的清理工作,但是因为GC工作规则的限制,GC调用对象的Finalize方法,第一次不会开释资源,第二次调用之后才删除对象。
(2)继续IDisposable接口,实现Dispose()方法,IDisposable接口定义了一个模式(具有语言级的支持),为开释未托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾收集器相关的题目。
为了更好的理解垃圾回收机制,我特意写了部门代码,里面添加了具体的注释。定义单个类FrankClassWithDispose(继续接口IDisposable)、FrankClassNoFinalize(没终结器)、FrankClassWithDestructor(定义了析构函数)。
详细代码如下:
其中使用了非托管的对象ODBCConnection的实例。建立的客户端进行了简朴的测试。客户端代码如下:
1using System;
2using System.Collections.Generic;
3using System.Text;
4using System.Data;
5using MemoryManagement;
6//Coded By Frank Xu Lei 18/2/2009
7//Study the .NET Memory Management
8//Test The Unmanaged Objects Reclaimed.
9//针对非托管代码的测试,比较
10//托管代码,GC可以更具策略自己回收,也可以实现IDisposable,调用Dispose()方法,主动开释。
11namespace MemoryManagementClient
12{
13 class Program
14 {
15 static void Main(string[] args)
16 {
17
18 /////////////////////////////////////////(1)////////////////////////////////////////////
19 //调用Dispose()方法,主动开释。资源,灵活
20 FrankClassWithDispose _frankClassWithDispose = null;
21 try
22 {
23 _frankClassWithDispose = new FrankClassWithDispose();
24 _frankClassWithDispose.DoSomething();
25
26 }
27 finally
28 {
29 if (_frankClassWithDispose!=null)
30 _frankClassWithDispose.Dispose();
31 //Console.WriteLine("FrankClassWithDispose实例已经被开释");
32 }
33
34 /////////////////////////////////////////(2)//////////////////////////////////////////////
35 //可以使用Using语句创建非托管对象,方法执行结束前,会调用
36 using (FrankClassWithDispose _frankClassWithDispose2 = new FrankClassWithDispose())
37 {
38 //_frankClassWithDispose2.DoSomething();
39 }
40
41 /////////////////////////////////////////(3)////////////////////////////////////////////
42 //垃圾收集器运行的时候,一次就开释资源
43 FrankClassNoFinalize _frankClassNoFinalize = new FrankClassNoFinalize();
44 _frankClassNoFinalize.DoSomething();
45
46 //////////////////////////////////////////(4)//////////////////////////////////////////////
47 //垃圾收集器运行的时候,两次才能够开释资源
48 FrankClassWithDestructor _frankClassWithDestructor = new FrankClassWithDestructor();
49 _frankClassWithDestructor.DoSomething();
50 ///////////////////////////////////////////(5)/////////////////////////////////////////////
51 //不能使用Using语句来创建对象,由于其没实现IDispose接口
52 //using (FrankClassWithDestructor _frankClassWithDestructor2 = new FrankClassWithDestructor())
53 //{
54 // _frankClassWithDestructor2.DoSomething();
55 //}
56
57 //////////////////////////////////////////////////////////////////////////////////////
58 //For Debug
59 Console.WriteLine("Press any key to continue");
60 Console.ReadLine();
61
62
63 }
64 }
65}
66
有些时候资源必需在特定时间开释,类可以实现执行资源治理和清除任务方法IDisposable.Dispose的接口IDisposable。
假如调用者需要调用Dispose方法清理对象,类作为契约的一部门必需实现Dispose方法。垃圾收集器默认情况下不会调用
Dispose方法;然而,实现Dispose方法可以调用GC里的方法去规范垃圾收器的终结行为。
值得一提的是:调用Dispose()方法,主动开释资源,灵活,可以使用Using语句创建非托管对象,方法执行结束前,会调用
Dispose()方法开释资源,这两端代码的效果是一样的,可以查看编译后IL。
1.try
2 {
3 IL_0003: nop
4 IL_0004: newobj instance void [MemoryManagement]MemoryManagement.FrankClassWithDispose::.ctor()
5 IL_0009: stloc.0
6 IL_000a: ldloc.0
7 IL_000b: callvirt instance void [MemoryManagement]MemoryManagement.FrankClassWithDispose::DoSomething()
8 IL_0010: nop
9 IL_0011: nop
10 IL_0012: leave.s IL_0028
11 } // end .try
12 finally
13 {
14 IL_0014: nop
15 IL_0015: ldloc.0
16 IL_0016: ldnull
17 IL_0017: ceq
18 IL_0019: stloc.s CS$4$0000
19 IL_001b: ldloc.s CS$4$0000
20 IL_001d: brtrue.s IL_0026
21 IL_001f: ldloc.0
22 IL_0020: callvirt instance void [MemoryManagement]MemoryManagement.FrankClassWithDispose::Dispose()
23 IL_0025: nop
24 IL_0026: nop
25 IL_0027: endfinally
26 } // end handler
27 IL_0028: nop
28 IL_0029: newobj instance void [MemoryManagement]MemoryManagement.FrankClassWithDispose::.ctor()
29 IL_002e: stloc.1
30 .try
31 {
32 IL_002f: nop
33 IL_0030: nop
34 IL_0031: leave.s IL_0045
35 } // end .try
36 finally
37 {
38 IL_0033: ldloc.1
39 IL_0034: ldnull
40 IL_0035: ceq
41 IL_0037: stloc.s CS$4$0000
42 IL_0039: ldloc.s CS$4$0000
43 IL_003b: brtrue.s IL_0044
44 IL_003d: ldloc.1
45 IL_003e: callvirt instance void [mscorlib]System.IDisposable::Dispose()
46 IL_0043: nop
47 IL_0044: endfinally
48 } // end handler
49
Using 语句有同样的效果,来实现非托管对象资源的开释。这点在口试中也会常常碰到,Using枢纽字的用法有哪几种等等类似的题目。基本理想的谜底都是除了引用命名空间,和命名空间设置别号外,就是这个用法实现如try finally块一样作用的对非托管对象资源的回收。只是一种简便的写法。
当你用Dispose方法开释未托管对象的时候,应该调用GC.SuppressFinalize。假如对象正在终结队列(finalization queue),GC.SuppressFinalize会阻止GC调用Finalize方法。由于Finalize方法的调用会牺牲部门机能。假如你的Dispose方法已经对委托管资源作了清理,就没必要让GC再调用对象的Finalize方法(MSDN)。附上MSDN的代码,大家可以参考. 总结:看了本文以后,不知对你是否有所匡助,假如你理解了.net垃圾回收的机制和GC的工作原理,以及包含如何治理非托管资源,你就会成为一个内存治理的高手。假如口试官问道这个题目,你就可以具体阐述你对这类题目的理解和看法。但愿这篇文章能对你的工作和学习带来匡助~
public class BaseResource: IDisposable
{
// 指向外部非托管资源
private IntPtr handle;
// 此类使用的其它托管资源.
private Component Components;
// 跟踪是否调用.Dispose方法,标识位,控制垃圾收集器的行为
private bool disposed = false;
// 构造函数
public BaseResource()
{
// Insert appropriate constructor code here.
}
// 实现接口IDisposable.
// 不能声明为虚方法virtual.
// 子类不能重写这个方法.
public void Dispose()
{
Dispose(true);
// 离开终结队列Finalization queue
// 设置对象的阻止终结器代码
//
GC.SuppressFinalize(this);
}
// Dispose(bool disposing) 执行分两种不同的情况.
// 假如disposing 即是 true, 方法已经被调用
// 或者间接被用户代码调用. 托管和非托管的代码都能被开释
// 假如disposing 即是false, 方法已经被终结器 finalizer 从内部调用过,
//你就不能在引用其他对象,只有非托管资源可以被开释。
protected virtual void Dispose(bool disposing)
{
// 检查Dispose 是否被调用过.
if(!this.disposed)
{
// 假如即是true, 开释所有托管和非托管资源
if(disposing)
{
// 开释托管资源.
Components.Dispose();
}
// 开释非托管资源,假如disposing为 false,
// 只会执行下面的代码.
CloseHandle(handle);
handle = IntPtr.Zero;
// 留意这里长短线程安全的.
// 在托管资源开释以后可以启动其它线程销毁对象,
// 但是在disposed标记设置为true前
// 假如线程安全是必需的,客户端必需实现。
}
disposed = true;
}
// 使用interop 调用方法
// 清除非托管资源.
[System.Runtime.InteropServices.DllImport("Kernel32")]
private extern static Boolean CloseHandle(IntPtr handle);
// 使用C# 析构函数来实现终结器代码
// 这个只在Dispose方法没被调用的条件下,才能调用执行。
// 假如你给基类终结的机会.
// 不要给子类提供析构函数.
~BaseResource()
{
// 不要重复创建清理的代码.
// 基于可靠性和可维护性考虑,调用Dispose(false) 是最佳的方式
Dispose(false);
}
// 答应你多次调用Dispose方法,
// 但是会抛出异常假如对象已经开释。
// 不论你什么时间处理对象都会核查对象的是否开释,
// check to see if it has been disposed.
public void DoSomething()
{
if(this.disposed)
{
throw new ObjectDisposedException();
}
}
对于需要调用Close方法比Dispose方法更加天然的类型,可以在 基类增加一个Close方法。
Close方法无参调用执行恰当清理工作的Dispose方法。
下面的例子演示了Close方法。
// 不要设置方法为virtual.
// 继续类不答应重写这个方法
public void Close()
{
// 无参数调用Dispose参数.
Dispose();
}
public static void Main()
{
// Insert code here to create
// and use a BaseResource object.
}
}