http://www.2cto.com/kf/201109/103302.html
http://www.cnblogs.com/tian2010/archive/2012/05/18/2508060.html
线程堆栈:简称栈 Stack
托管堆: 简称堆 Heap
使用.Net框架开发程序的时候,我们无需关心内存分配问题,因为有GC这个大管家给我们料理一切。如果我们写出如下两段代码:
代码段1:
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
代码段2:
public class MyInt
{
public int MyValue;
}
public MyInt AddFive(int pValue)
{
MyInt result = new MyInt();
result.MyValue = pValue + 5;
return result;
}
问题1:你知道代码段1在执行的时候,pValue和result在内存中是如何存放,生命周期又如何?代码段2呢?
要想释疑以上问题,我们就应该对.Net下的栈(Stack)和托管堆(Heap)(简称堆)有个清楚认识,本立而道生。如果你想提高程序性能,理解栈和堆,必须的!
本文就从栈和堆,类型变量展开,对我们写的程序进行庖丁解牛。
C#程序在CLR上运行的时候,内存从逻辑上划分两大块:栈,堆。这俩基本元素组成我们C#程序的运行环境。
一,栈 vs 堆:区别?
栈通常保存着我们代码执行的步骤,如在代码段1中 AddFive()方法,int pValue变量,int result变量等等。而堆上存放的则多是对象,数据等。(译者注:忽略编译器优化)我们可以把栈想象成一个接着一个叠放在一起的盒子。当我们使用的时候,每次从最顶部取走一个盒子。栈也是如此,当一个方法(或类型)被调用完成的时候,就从栈顶取走(called a Frame,译注:调用帧),接着下一个。堆则不然,像是一个仓库,储存着我们使用的各种对象等信息,跟栈不同的是他们被调用完毕不会立即被清理掉。
如图1,栈与堆示意图
(图1)
栈内存无需我们管理,也不受GC管理。当栈顶元素使用完毕,立马释放。而堆则需要GC(Garbage collection:垃圾收集器)清理。
二,什么元素被分配到栈?什么被分配到堆?
当我们程序执行的时候,在栈和堆中分配有四种主要的类型:值类型,引用类型,指针,指令。
值类型:
在C#中,继承自System.ValueType的类型被称为值类型,主要有以下几种(CLR2.0中支持类型有增加):
* bool
* byte
* char
* decimal
* double
* enum
* float
* int
* long
* sbyte
* short
* struct
* uint
* ulong
* ushort
引用类型:
以下是引用类型,继承自System.Object:
* class
* interface
* delegate
* object
* string
指针:
在内存区中,指向一个类型的引用,通常被称为“指针”,它是受CLR( Common Language Runtime:公共语言运行时)管理,我们不能显示使用。需要注意的是,一个类型的引用即指针跟引用类型是两个完全不同的概念。指针在内存中占一块内存区,它本身只代表一个内存地址(或者null),它所指向的另一块内存区才是我们真正的数据或者类型。如图2:
(图2)
指令:
后文对指令再做介绍。
三,如何分配?
我们先看一下两个观点:
观点1,引用类型总是被分配在堆上。(正确?)
观点2,值类型和指针总是分配在被定义的地方,他们不一定被分配到栈上。(这个理解起来有点难度,需要慢慢来)
上文提及的栈(Stack),在程序运行的时候,每个线程(Thread)都会维护一个自己的专属线程堆栈。
当一个方法被调用的时候,主线程开始在所属程序集的元数据中,查找被调用方法,然后通过JIT即时编译并把结果(一般是本地CPU指令)放在栈顶。CPU通过总线从栈顶取指令,驱动程序以执行下去。
下面我们以实例来详谈。
还是我们开篇所列的代码段1:
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
当AddFive方法开始执行的时候,方法参数(parameters)则在栈上分配。如图3:
(图3)
注意:方法并不在栈中存活,图示仅供参考。
接着,指令指向AddFive方法内部,如果该方法是第一次执行,首先要进行JIT即时编译。如图4:
(图4)
当方法内部开始执行的时候,变量result被分配在栈上,如图5:
(图5)
方法执行完毕,而且方法返回后,如图6所示:
(图6)
在方法执行完毕返回后,栈上的区域被清理。如图7:
(图7)
以上看出,一个值类型变量,一般会分配在栈上。那观点2中所述又做何理解?“值类型和指针总是分配在被定义的地方,他们不一定被分配到栈上”。
原因就是如果一个值类型被声明在一个方法体外并且在一个引用类型中,那它就会在堆上进行分配。
还是代码段2:
public class MyInt
{
public int MyValue;
}
public MyInt AddFive(int pValue)
{
MyInt result = new MyInt();
result.MyValue = pValue + 5;
return result;
}
当线程开始执行AddFive方法的时候,参数被分配到栈上,如图8所示:
(图8)
由于MyInt是一个引用类型,所以它被分配到堆上,并且在栈中生成一个指针(result),如图9:
(图9)
AddFive方法执行完毕时的情况如图10:
(图10)
栈上内存被清理,堆中依然存在,如图11:
(图11)
当程序需要更多的堆空间时,GC需要进行垃圾清理工作,暂停所有线程,找出所有不可达到对象,即无被引用的对象,进行清理。并通知栈中的指针重新指向地址排序后的对象。现在我们应该知道,了解栈和堆,对我们开发出高性能程序的重要性。当我们使用引用类型的时候,一般是对指针进行的操作而非引用类型对象本身。但是值类型则操作其本身。
接下来,我们用例子说明这一点。
例1:
public int ReturnValue()
{
int x = new int();
x = 3;
int y = new int();
y = x;
y = 4;
return x;
}
执行结果为3,稍作修改:
例2:
public class MyInt
{
public int MyValue;
}
public int ReturnValue2()
{
MyInt x = new MyInt();
x.MyValue = 3;
MyInt y = new MyInt();
y = x;
y.MyValue = 4;
return x.MyValue;
}
执行结果为4。
我们来分析下原因,其实例1的跟以下代码所起效用一样:
public int ReturnValue()
{
int x = 3;
int y = x;
y = 4;
return x;
}
如图12所示,在栈上x和y分别占用一块内存区,互不干扰。
(图12)
而例2,与以下代码所起效用一样:
public int ReturnValue2()
{
MyInt x;
x.MyValue = 3;
MyInt y;
y = x;
y.MyValue = 4;
return x.MyValue;
}
如图13所示,
尽管在.NET framework下我们并不需要担心内存管理和垃圾回收(Garbage Collection),但是我们还是应该了解它们,以优化我们的应用程序。同时,还需要具备一些基础的内存管理工作机制的知识,这样能够有助于解释我们 日常程序编写中的变量的行为。在本文中我将讲解我们必须要注意的方法传参的行为。
在第一部分里我介绍了栈和堆的基本功能,还介绍到了在程序执行时值类型和引用类型是如何分配的,而且还谈到了指针。
* 参数,大问题
这里有一个代码执行时的详细介绍,我们将深入第一部分出现的方法调用过程...
当我们调用一个方法时,会发生以下的事情:
1.方法执行时,首先在栈上为对象实例中的方法分配空间,然后将方法拷贝到栈上(此时的栈被称为帧),但是该空间中只存放了执行方法的指令,并没有方法内的数据项。
2.方法的调用地址(或者说指针)被放置到栈上,一般来说是一个GOTO指令,使我们能够在方法执行完成之后,知道回到哪个地方继续执行程序。(最好能理解这一点,但并不是必须的,因为这并不会影响我们的编码)
3.方法参数的分配和拷贝是需要空间的,这一点是我们需要进一步注意。
4.控制此时被传递到了帧上,然后线程开始执行我们的代码。因此有另一个方法叫做"调用栈"。
示例代码如下:
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
此时栈开起来是这样的:
就像第一部分讨论的那样,放在栈上的参数是如何被处理的,需要看看它是值类型还是引用类型。值类型的值将被拷贝到栈上,而引用类型的引用(或者说指针)将被拷贝到栈上。
* 值类型传递
首先,当我们传递一个值类型参数时,栈上被分配好一个新的空间,然后该参数的值被拷贝到此空间中。
来看下面的方法:
class Class1
{
public void Go()
{
int x = 5;
AddFive(x);
Console.WriteLine(x.ToString());
}
public int AddFive(int pValue)
{
pValue += 5;
return pValue;
}
}
方法Go()被放置到栈上,然后执行,整型变量"x"的值"5"被放置到栈顶空间中。
然后AddFive()方法被放置到栈顶上,接着方法的形参值被拷贝到栈顶,且该形参的值就是"x"的拷贝。
当AddFive()方法执行完成之后,线程就通过预先放置的指令返回到Go()方法的地址,然后从栈顶依次将变量pValue和方法AddFive()移除掉:
所以我们的代码输出的值是"5",对吧?这里的关键之处就在于任何传入方法的值类型参数都是复制拷贝的,所以原始变量中的值是被保留下来而没有被改变的。
必须注意的是,如果我们要将一个非常大的值类型数据(如数据量大的struct类型)入栈,它会占用非常大的内存空间,而且会占有过多的处理器周期 来进行拷贝复制。栈并没有无穷无尽的空间,它就像在水龙头下盛水的杯子,随时可能溢出。struct是一个能够存放大量数据的值类型成员,我们必须小心地 使用。
这里有一个存放大数据类型的struct:
public struct MyStruct
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
来看看当我们执行了Go()和DoSometing()方法时会发生什么:
public void Go()
{
MyStruct x = new MyStruct();
DoSomething(x);
}
public void DoSomething(MyStruct pValue)
{
// DO SOMETHING HERE....
}
这将会非常的低效。想象我们要是传递2000次MyStruct,你就会明白程序是怎么瘫痪掉的了。
那么我们应该如何解决这个问题?可以通过下列方式来传递原始值的引用:
public void Go()
{
MyStruct x = new MyStruct();
DoSomething(ref x);
}
public struct MyStruct
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
public void DoSomething(ref MyStruct pValue)
{
// DO SOMETHING HERE....
}
通过这种方式我们能够提高内存中对象分配的效率。
唯一需要注意的是,在我们通过引用传递值类型时我们会修改该值类型的值,也就是说pValue值的改变会引起x值的改变。执行以下代码,我们的结果会变成"123456",这是因为pValue实际指向的内存空间与x变量声明的内存空间是一致的。
public void Go()
{
MyStruct x = new MyStruct();
x.a = 5;
DoSomething(ref x);
Console.WriteLine(x.a.ToString());
}
public void DoSomething(ref MyStruct pValue)
{
pValue.a = 12345;
}
* 引用类型传递
传递引用类型参数的情况类似于先前例子中通过引用来传递值类型的情况。
如果我们使用引用类型:
public class MyInt
{
public int MyValue;
}
然后调用Go()方法,MyInt对象将放置在堆上:
public void Go()
{
MyInt x = new MyInt();
}
如果我们执行下面的Go()方法:
public void Go()
{
MyInt x = new MyInt();
x.MyValue = 2;
DoSomething(x);
Console.WriteLine(x.MyValue.ToString());
}
public void DoSomething(MyInt pValue)
{
pValue.MyValue = 12345;
}
将发生这样的事情...
1.方法Go()入栈
2.Go()方法中的变量x入栈
3.方法DoSomething()入栈
4.参数pValue入栈
5.x的值(MyInt对象的在栈中的指针地址)被拷贝到pValue中
因此,当我们通过MyInt类型的pValue来改变堆中MyInt对象的MyValue成员值后,接着又使用指向该对象的另一个引用x来获取了其MyValue成员值,得到的值就变成了"12345"。
而更有趣的是,当我们通过引用来传递一个引用类型时,会发生什么?
让我们来检验一下。假如我们有一个"Thing"类和两个继承于"Thing"的"Animal"和"Vegetable" 类:
public class Thing
{
}
public class Animal:Thing
{
public int Weight;
}
public class Vegetable:Thing
{
public int Length;
}
然后执行下面的Go()方法:
public void Go()
{
Thing x = new Animal();
Switcharoo(ref x);
Console.WriteLine(
"x is Animal : "
+ (x is Animal).ToString());
Console.WriteLine(
"x is Vegetable : "
+ (x is Vegetable).ToString());
}
public void Switcharoo(ref Thing pValue)
{
pValue = new Vegetable();
}
变量x被返回为Vegetable类型。
x is Animal : False
x is Vegetable : True
让我们来看看发生了什么:
1.Go()方法入栈
2.x指针入栈
3.Animal对象实例化到堆中
4.Switcharoo()方法入栈
5.pValue入栈且指向x
6.Vegetable对象实例化到堆中
7.x的值通过被指向Vegetable对象地址的pValue值所改变。
如果我们不使用Thing的引用,相反的,我们得到结果变量x将会是Animal类型的。
如果以上代码对你来说没有什么意义,那么请继续看看我的文章中关于引用变量的介绍,这样能够对引用类型的变量是如何工作的会有一个更好的理解。
我们看到了内存是怎样处理参数传递的,在系列的下一部分中,我们将看看栈中的引用变量发生了些什么,然后考虑当我们拷贝对象时是如何来解决某些问题的。
A Copy Is Not A Copy
为了清楚的说明这个问题,我们来校验以下两种情形将会发生什么情况:位于堆里面的一个值类型和位于堆里面的一个引用类型。我们先看值类型。看下面的class和struct。我们有一个Dude类,该类包含一个Name元素和两个Shoe(s)。同时,添加一个CopyDude()方法来更加方便的new Dudes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
public struct Shoe{ public string Color; } public class Dude { public string Name; public Shoe RightShoe; public Shoe LeftShoe; public Dude CopyDude() { Dude newPerson = new Dude(); newPerson.Name = Name; newPerson.LeftShoe = LeftShoe; newPerson.RightShoe = RightShoe; return newPerson; } public override string ToString() { return (Name + " : Dude!, I have a " + RightShoe.Color + " shoe on my right foot, and a " + LeftShoe.Color + " on my left foot." ); } } |
1
|
|
1
|
我们的Dude是一个引用类型,但是Shoe是一个结构体,属于值类型,在堆栈上的反应是这样的: |
1
|
|
1
|
当我们执行以下方法的时候: |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public static void Main() { Class1 pgm = new Class1(); Dude Bill = new Dude(); Bill.Name = "Bill" ; Bill.LeftShoe = new Shoe(); Bill.RightShoe = new Shoe(); Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue" ; Dude Ted = Bill.CopyDude(); Ted.Name = "Ted" ; Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red" ; Console.WriteLine(Bill.ToString()); Console.WriteLine(Ted.ToString()); } |
我们得到了预期的结果:
Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot.
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot.
如果我们把Shoe改成引用类型,会是怎样一种情况呢?问题就在于此。我们将Shoe改成引用类型(class):
1
2
3
4
|
public class Shoe{ public string Color; } |
让后再次执行Main()方法,我们得到的结果是:
Bill : Dude!, I have a Red shoe on my right foot, and a Red on my left foot
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot
注意到红色部分,这显然是一个错误。这是我们在堆里面得到的结果:
由于我们现在使用Shoe作为引用类型而不是值类型,而,当我们拷贝引用类型的内容时,我们只是拷贝了指针。我们必须做一些额外的工作,让我们的引用类型看起来更像是值类型。
幸运的是,我们有一个接口可以帮助我们解决这个问题:ICloneable。这个接口是所有的Dudes都遵守的契约,并且定义了一个应用类型是如何复制的,以防止我们的”共享鞋(shoe sharing)”的错误。所有的类要能被clone,都需要实现ICloneable接口,我们的Shoe也不例外。
ICloneable包含一个方法:Clone():
1
2
3
4
5
|
public object Clone() { } |
我们将Shoe类改成如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class Shoe : ICloneable { public string Color; #region ICloneable Members public object Clone() { Shoe newShoe = new Shoe(); newShoe.Color = Color.Clone() as string ; return newShoe; } #endregion } |
在Clone方法中,我们只是new了一个Shoe,克隆了所有的引用类型和拷贝了所有的值类型,然后返回一个新的object。也许你已经注意到了,string已经实现了ICloneable,因此我们可以直接调用Color.Clone()。由于Clone()方法是返回引用类型的,因此,在我们设定shoe的color之前我们必须重新设定引用的类型(retype the reference).
下一步,在我们的CopyDude()方法中,我们需要克隆shoes而不是拷贝他们:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public Dude CopyDude() { Dude newPerson = new Dude(); newPerson.Name = Name; newPerson.LeftShoe = LeftShoe.Clone() as Shoe; newPerson.RightShoe = RightShoe.Clone() as Shoe; return newPerson; } |
现在,当我们运行Main()方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public static void Main() { Class1 pgm = new Class1(); Dude Bill = new Dude(); Bill.Name = "Bill" ; Bill.LeftShoe = new Shoe(); Bill.RightShoe = new Shoe(); Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue" ; Dude Ted = Bill.CopyDude(); Ted.Name = "Ted" ; Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red" ; Console.WriteLine(Bill.ToString()); Console.WriteLine(Ted.ToString()); } |
我们得到的结果:
Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot
这正是我们需要的:
将东西包装起来(Wrapping Things Up)
作为通常的情况,我们总是想要克隆引用类型和拷贝值类型。(这将减少你为了防止头疼而不得不买的阿司匹林,当你调试这些错误的时候)
为了减少令人头痛的事,让我们更进一步,来整理Dude类,使它是想ICloneable,而不是使用CopyDude()方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
public class Dude: ICloneable { public string Name; public Shoe RightShoe; public Shoe LeftShoe; public override string ToString() { return (Name + " : Dude!, I have a " + RightShoe.Color + " shoe on my right foot, and a " + LeftShoe.Color + " on my left foot." ); } #region ICloneable Members public object Clone() { Dude newPerson = new Dude(); newPerson.Name = Name.Clone() as string ; newPerson.LeftShoe = LeftShoe.Clone() as Shoe; newPerson.RightShoe = RightShoe.Clone() as Shoe; return newPerson; } #endregion } |
我们也需要修改Main()方法来使用Dude.Clone():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
public static void Main() { Class1 pgm = new Class1(); Dude Bill = new Dude(); Bill.Name = "Bill" ; Bill.LeftShoe = new Shoe(); Bill.RightShoe = new Shoe(); Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue" ; Dude Ted = Bill.Clone() as Dude; Ted.Name = "Ted" ; Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red" ; Console.WriteLine(Bill.ToString()); Console.WriteLine(Ted.ToString()); } |
我们最后的输出是:
Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot.
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot.
一切都正常。
一些值得注意的有趣的事是System.String的赋值操作(=操作符)事实上克隆了string,所以你不必担心重复的引用。然而,你需要注意防止内存膨胀。如果你看了上面的图标,由于string是一个引用类型,它需要指向堆里的另外一个对象,但为了简单,它显示成一个值类型。
总结:
通常的,如果我们有打算拷贝我们的对象,我们需要实现ICloneable。这样可以使我们的引用类型在行为上看起来有点像值类型。正如你所看到的,记录我们正在处理的是那种类型变量是很重要的,因为值类型和引用类型在分配内存时是不一样的。
绘图(Graphing)
让我们从GC的角度来看这个问题。如果我们的目标是“移除垃圾”,那么我们需要一个计划,使之更有效。显然,我们需要判断什么是垃圾,什么不是(对于有敛癖(pack-rats)的人,这可能就有点痛苦了。)。为了决定什么该被保留,我们首先假定,任何没有被使用的即使垃圾(墙角成堆的旧报纸、阁楼上的舢板、衣柜里所有的东西,等等)。假设我们有2个朋友,Joseph Ivan Thomas (JIT) 和Cindy Lorraine Richmond (CLR)。Joe和Cindy记录哪些是正在使用的,并且将他们需要的做成一个列表给我们。我们将这些原始的列表称之为”根”列表(“root
”list)因为我们将它作为起始点.我们将保留一份主要的列表,用来描述哪些使我们要保留在屋内的。为了使我们的列表能正常工作的,都将被添加到图标中(如果我们保留了电视,就不应该丢掉电视遥控器,因此,遥控器也将被添加到列表里面。如果我们保留了电脑,那么键盘和显示器也将列入”保留”列表)。
这也是GC如何决定该保留哪些东西。它从JIT(即时编译器)和CLR(公共语言运行时)获取一份需要保留的”根”对象引用(还记得Joe和Cindy吧?) ,然后通过递归搜索对象引用以建立一份我们需要保留的图标。
根列表包括:
- 全局的/静态的指针。保证我们的对象没有被垃圾回收的一个方法是保持一个静态变量对其引用。
- 栈上的指针。我们不会丢弃执行应用程序进程所需的东西。
- CPU注册指针。位于托管堆的被CPU的内存地址所指向的任何东西都应该保留。
在上图中,位于托管堆的对象1,3和5被根所引用,其中,1和5是直接引用,3是递归搜索时找到的。类比我们上面的例子,对象1是电视,3就是遥控器。所有的对象都用图形表示出来后,我们将进入下一步,压缩。
压缩
现在我们已经画出哪些对象是我们要保留的,我们可以仅移动要保留的对象来压缩。
幸运的是,在我们的屋里,在我们把其他东西放进来,不需要进行空间清理。既然对象2是不需要的,我们将对象3往下移,同时修复对象1的指针。
下一步,我们将对象5往下移:
既然一切都已清理完毕,我们只需写个便利贴放到压缩的堆上,让Claire知道,新的对象应该放在哪里。
知道GC的本质将有助有我们理解移动对象是非常费力的。正如你所看到的,如果我们能够缩小我们不得不盈动的对象,那将是非常有意义的,我们将改善GC的整个处理过程,因为这将减少拷贝。
托管堆外的对象如何处理呢?
当一个人负责处理垃圾的时候,打扫屋里的时候,我们就会碰到一个问题,如何处理车里面的东西呢?既然要清理,我们就需要清理所有的东西。如果轻便电脑在屋里,但是电池在车上,怎么办呢?
有一些情形,GC需要执行代码用于清除非托管的资源,如,文件、数据库连接、网络连接等,处理这种情形的一种可行方案是通过终止化器(finalizer).
1
2
3
4
5
6
7
8
9
10
11
|
class Sample { ~Sample() { // FINALIZER: CLEAN UP HERE } } |
当对象被创建的时候,所有的对象连同一个终止化器将一起被添加到终止化列表里。我们假设对象1,4和5有终止化器并且被加到终止化列表。我们来看一下当对象2和4已经不再被程序引用并且将被垃圾回收时,会发生怎样的情况。
对象2将被视为正常情况的处理。对于对象4,GC发现其位于终止化列表,对象4将被移动,同时它的终结器将被添加到特殊的队列里:这个队列叫做终止化可达队列(freachable)
CLR中有一个专属的线程用来处理终止化可达队列中的项目。当对象4的终止化器被这个线程执行后,它将被移出终止化可达队列,当且仅当这个时候,对象4才准备被回收。
因此,对象4将一直存在,直到下一轮的GC操作。
由于添加一个终结器到我们的类里面将为GC增加额外的工作,这将给垃圾回收和我们的程序的性能带来昂贵的和不利的影响。只有当你非常确定的时候,才使用终结器。
一个好的实践是,确保清楚非托管的资源。你可以预见到的,最好是明确的关闭连接,并尽可能用IDisposable接口来清除而不是用终结器。
IDisposable
实现了IDisposable接口的类将在Dispose()方法(该接口的唯一签名)中进行资源清理。因此,如果我们有一个ResourceUser,不采用如下的终结器:
1
2
3
4
5
6
7
8
9
10
11
|
public class ResourceUser { ~ResourceUser() // THIS IS A FINALIZER { // DO CLEANUP HERE } } |
我们可以使用一种更好的方式IDisposable,来实现相同的功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class ResourceUser : IDisposable { #region IDisposable Members public void Dispose() { // CLEAN UP HERE!!! } #endregion } |
IDisposable 被集成到了using关键字中。在using块的最后,将调用在using中声明的对象的Dispose()方法。该对象在using代码块之后不应再被使用,因为它应该是不存在的,等待垃圾回收的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public static void DoSomething() { ResourceUser rec = new ResourceUser(); using (rec) { // DO SOMETHING } // DISPOSE CALLED HERE // DON'T ACCESS rec HERE } |
我更喜欢将对象的声明放到using里面,因为它更容易理解,并且rec在using代码块外将不再可用。这种在写成一行的模式更符合IDisposable的意图,但他不是必须的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public static void DoSomething() { using (ResourceUser rec = new ResourceUser()) { // DO SOMETHING } // DISPOSE CALLED HERE } |
通过对实现了IDisposable的类使用using(),我们可以执行资源清理工作,而不用强迫GC终结我们的对象付出额外的负担。
静态变量:小心!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class Counter { private static int s_Number = 0; public static int GetNextNumber() { int newNumber = s_Number; // DO SOME STUFF s_Number = newNumber + 1; return newNumber; } } |
如果有2个线程同时调用了GetNextNumber()方法,并且都在s_Number增加之前给newNumber赋了相同的值,那么他们将返回相同的结果。有一种方法可以保证在同一时间里,只有一个线程可以进入到一个代码块里。作为最佳实践,你应该锁住尽可能小的代码块,因为其他线程必须排队等候lock住的代码。而这将是低效的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
class Counter { private static int s_Number = 0; public static int GetNextNumber() { lock ( typeof (Counter)) { int newNumber = s_Number; // DO SOME STUFF newNumber += 1; s_Number = newNumber; return newNumber; } } } |
静态变量:小心!…第二种情形
下一个我们需要注意的事情是被静态变量所引用的对象.记住,任何被”根”所引用的对象将不会被清除。这是我能想到的最丑恶的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
class Olympics { public static Collection<Runner> TryoutRunners; } class Runner { private string _fileName; private FileStream _fStream; public void GetStats() { FileInfo fInfo = new FileInfo(_fileName); _fStream = _fileName.OpenRead(); } } |
对于Olympics类来说,Runner集合是静态的,不仅是集合中的对象不会被垃圾回收(它们被根间接引用),也许你也注意到了,每次我们执行GetStats的时候,stream就对文件开放,由于我们没有关闭,并且GC也不会释放,这段代码将会发生一场灾难。想象下,我们有100,000个runners。那么我们将以许多无法回收的对象,并且每个对象都有开放的资源而结束。天啊,糟糕的性能。
单例
保留东西轻量的一个有效途径是总是在内存中保存类的一个实例。一种简单的方式是使用GOF的单例模式。单例应该谨慎使用,因为他们是真正的”全局变量”,并且在多线程中,将导致许多头疼和”奇怪”的行为,因为不同的线程都可能改变对象的状态。如果我们使用单例模式(或其他全局变量),我们应该能够证明这是正当的用法。(换句话说,如果没有好的理由,请不要使用它。)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public Earth{ private static Earth _instance = new Earth(); private Earth() { } public static Earth GetInstance() { return _instance; } } |
我们有一个私有的构造函数,因此只有在Earth里面可以调用构造函数来构建Earth,我们还有一个Earth的静态实例和一个GetInstance方法,这种特别的实现方式是线程安全的,因为CLR保证静态变量的创建是线程安全的。这是我在C#里面法相的单例的最优雅的实现方式。
总结:
总结包装一下,提升GC性能,我们可以做的事情有:
- 清除。不要留下打开的资源。确保关闭所有的连接并且尽可能的清楚所有的非托管对象。作为使用非托管对象的一个通常规则,尽可能晚的实例化,并尽早的清除。
- 不要过度引用。使用引用对象要有理由。记住,如果我们的对象还存活,所有它引用的对象将不会被回收(如此循环下去)。当完成对类的某个引用,我们可以将其设成null来移除引用。我更倾向的一种严格的方法是,将不再需要的引用设置成自定义的轻量型的NullObject以防止空引用异常。GC开始工作的时候,留下越少的引用,操作处理将越没有压力。
- 慎重使用终结器。GC过程中,终结器开销是很大的,只有当我们可以证明这是合法的,才应该使用终结器。如果我们可以使用IDisposable代替终结器,那将更加有效,因为我们的对象将在一轮GC中被清除,而不需要第二轮。
- 保持对象和子对象一起。对于GC来说,相对于每次处理堆上的碎片来说,移动大块的内存时相对容易的。因此,当我们声明一个包含多个组合对象的对象时,应将这些组合对象尽可能在一个地方实例化。
The End.