今天抽空来讨论一下.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(定义了析构函数)。
具体代码如下:

1using System;

2using System.Collections.Generic;

3using System.Text;

4using System.Data;

5using System.Data.Odbc;

6using System.Drawing;

7
//Coded By Frank Xu Lei 18/2/2009

8
//Study the .NET Memory Management

9
//Garbage Collector 垃圾收集器。可以根据策略在需要的时候回收托管资源,

10
//但是GC不知道如何管理非托管资源。如网络连接、数据库连接、画笔、组件等

11
//两个机制来解决非托管资源的释放问题。析构函数、IDispose接口

12
//COM引用计数

13
//C++手动管理,New Delete

14
//VB自动管理

15namespace MemoryManagement

16{

17
//继承接口IDisposable,实现Dispose方法,可以释放FrankClassDispose的实例资源

18
public
class FrankClassWithDispose : IDisposable

19 {

20
private OdbcConnection _odbcConnection =
null;

21

22
//构造函数

23
public FrankClassWithDispose()

24 {

25
if (_odbcConnection ==
null)

26 _odbcConnection =
new OdbcConnection();

27 Console.WriteLine(
"FrankClassWithDispose has been created ");

28 }

29
//测试方法

30
public
void DoSomething()

31 {

32

33 /**
/////code here to do something

34
return ;

35 }

36
//实现Dispose,释放本类使用的资源

37
public
void Dispose()

38 {

39
if (_odbcConnection !=
null)

40 _odbcConnection.Dispose();

41 Console.WriteLine(
"FrankClassWithDispose has been disposed");

42 }

43 }

44
//没有实现Finalize,等着GC回收FrankClassFinalize的实例资源,GC运行时候直接回收

45
public
class FrankClassNoFinalize

46 {

47
private OdbcConnection _odbcConnection =
null;

48
//构造函数

49
public FrankClassNoFinalize()

50 {

51
if (_odbcConnection ==
null)

52 _odbcConnection =
new OdbcConnection();

53 Console.WriteLine(
"FrankClassNoFinalize has been created");

54 }

55
//测试方法

56
public
void DoSomething()

57 {

58

59
//GC.Collect();

60 /**
/////code here to do something

61
return ;

62 }

63 }

64
//实现析构函数,编译为Finalize方法,调用对象的析构函数

65
//GC运行时,两次调用,第一次没释放资源,第二次才释放

66
//FrankClassDestructor的实例资源

67
//CLR使用独立的线程来执行对象的Finalize方法,频繁调用会使性能下降

68
public
class FrankClassWithDestructor

69 {

70
private OdbcConnection _odbcConnection =
null;

71
//构造函数

72
public FrankClassWithDestructor()

73 {

74
if (_odbcConnection ==
null)

75 _odbcConnection =
new OdbcConnection();

76 Console.WriteLine(
"FrankClassWithDestructor has been created");

77 }

78
//测试方法

79
public
void DoSomething()

80 {

81 /**
/////code here to do something

82

83
return ;

84 }

85
//析构函数,释放未托管资源

86 ~FrankClassWithDestructor()

87 {

88
if (_odbcConnection !=
null)

89 _odbcConnection.Dispose();

90 Console.WriteLine(
"FrankClassWithDestructor has been disposed");

91 }

92 }

93}

94
其中使用了非托管的对象
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的工作原理,以及包含如何管理非托管资源,你就会成为一个内存管理的高手。如果面试官问道这个问题,你就可以详细阐述你对这类问题的理解和看法。希望这篇文章能对你的工作和学习带来帮助~
本文转自 frankxulei 51CTO博客,原文链接:http://blog.51cto.com/frankxulei/318541,如需转载请自行联系原作者