既然有垃圾回收,肯定就有垃圾制造者在制造垃圾。这些垃圾是如何被制造出来的,就是我们今天试图弄清楚的问题。希望能通过以下文字描述清楚,程序运行时各阶段的内存使用状态。
阅读本章前,要求读者对值类型与引用类型有相当的了解
关于值类型与引用类型的深入讲解,请参见ANYTAO的[你必须知道的.NET] 第八回:品味类型---值类型与引用类型(上)-内存有理
----.NET应用程序初始化的时候所有的引用类型都会被分配到托管堆上面。而只有托管堆里面的资源才能被垃圾回收机制给管理起来。所以,我们要明白,CLR的垃圾回收机制并不是针对所有数据的。我们该手动释放的时候必须得要手动释放。
我们从何得知这一结论?如何论证这一点?
程序是如何在CLR中运行的呢?那我们CLR中垃圾回收机制又是怎么样运行的呢?我们是否需要再进一步去研究呢?我们如果不手动释放值类型,它们会常驻我们宝贵的内存空间吗?我不得不承认,这里牵扯到了很复杂的算法和操作系统及Win32编程的相关知识。这可能需要大篇幅的文字来说明这些问题。
既然有垃圾回收,肯定就有垃圾制造者在制造垃圾。这些垃圾是如何被制造出来的,就是我们今天试图弄清楚的问题。希望能通过以下文字描述清楚,程序运行时各阶段的内存使用状态。
请看下面的代码:
class Program
{
static void Main(string[] args)
{
//值类型
char i = new char();
char j = 'a';
//引用类型
StringBuilder sb = new StringBuilder();
//自定义类型
MyClass mc = new MyClass();
//系统的ENUM类型
StringComparison sc;
//自定义的enum类型
MyEnum me = MyEnum.test;
//结构体
MyStruct ms1;
ms1.T = "hi";
//在这里进行的对象复制
MyStruct ms2 = ms1;
ms1.T = "hi2";
MyStruct ms3 = new MyStruct();
char k = 'b';
if (ms2.Equals(ms1))
{
Console.WriteLine("实例相同" + ms2.T);
}
Console.WriteLine(ms1.T);
Console.WriteLine(ms2.T);
Console.WriteLine(ms3.T);
}
}
class MyClass
{
}
enum MyEnum
{
test,
}
struct MyStruct
{
public string T;
}
很明显,上面定义了一个类,一个枚举,一个结构,这是一段表面看似很普通的代码,但其实骨子里却大有乾坤。我们再次打开ILdasm来看下它的真面目。
可以看的出,两个类都有.ctor,就是说,两个类都有构造函数。点开会发现原来是继承自Object的构造函数。这也就解释了为什么我们定义类的时候就算不显示声明构造函数,系统也会自动给你声明构造函数。功劳来自于CLR.
细心的朋友又会发现另外一个问题,那这里这个结构明明没有看见构造函数,那它为什么也能new呢?关于这里,可以参考anytao关于new的文章
我这里谈谈自己的看法。我们点开Main函数。会看到如下内容
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 2
.locals init ([0] char i,
[1] char j,
[2] class [mscorlib]System.Text.StringBuilder sb,
[3] class CLR.MyClass mc,
[4] valuetype [mscorlib]System.StringComparison sc,
[5] valuetype CLR.MyEnum me,
[6] valuetype CLR.MyStruct ms1,
[7] valuetype CLR.MyStruct ms2,
[8] valuetype CLR.MyStruct ms3,
[9] char k,
[10] bool CS$4$0000)
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
IL_0003: ldc.i4.s 97
IL_0005: stloc.1
IL_0006: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
IL_000b: stloc.2
IL_000c: newobj instance void CLR.MyClass::.ctor()
IL_0011: stloc.3
IL_0012: ldc.i4.0
IL_0013: stloc.s me
IL_0015: ldloca.s ms1
IL_0017: ldstr "hi"
IL_001c: stfld string CLR.MyStruct::T
IL_0021: ldloc.s ms1
IL_0023: stloc.s ms2
IL_0025: ldloca.s ms1
IL_0027: ldstr "hi2"
IL_002c: stfld string CLR.MyStruct::T
IL_0031: ldloca.s ms3
IL_0033: initobj CLR.MyStruct
IL_0039: ldc.i4.s 98
IL_003b: stloc.s k
IL_003d: ldloca.s ms2
IL_003f: ldloc.s ms1
IL_0041: box CLR.MyStruct
IL_0046: constrained. CLR.MyStruct
IL_004c: callvirt instance bool [mscorlib]System.Object::Equals(object)
IL_0051: ldc.i4.0
IL_0052: ceq
IL_0054: stloc.s CS$4$0000
IL_0056: ldloc.s CS$4$0000
IL_0058: brtrue.s IL_0073
IL_005a: nop
IL_005b: ldstr bytearray (9E 5B 8B 4F F8 76 0C 54 ) // .[.O.v.T
IL_0060: ldloca.s ms2
IL_0062: ldfld string CLR.MyStruct::T
IL_0067: call string [mscorlib]System.String::Concat(string,
string)
IL_006c: call void [mscorlib]System.Console::WriteLine(string)
IL_0071: nop
IL_0072: nop
IL_0073: ldloca.s ms1
IL_0075: ldfld string CLR.MyStruct::T
IL_007a: call void [mscorlib]System.Console::WriteLine(string)
IL_007f: nop
IL_0080: ldloca.s ms2
IL_0082: ldfld string CLR.MyStruct::T
IL_0087: call void [mscorlib]System.Console::WriteLine(string)
IL_008c: nop
IL_008d: ldloca.s ms3
IL_008f: ldfld string CLR.MyStruct::T
IL_0094: call void [mscorlib]System.Console::WriteLine(string)
IL_0099: nop
IL_009a: ret
} // end of method Program::Main
可以看到,在init的时候,里面传入了11个参数。分别就是我们代码里使用的11个对象。Init指令起的作用是“init specifies that the variables must be initialized to the default values for their respective types” init 指定的变量必须被各自类型的默认值初始化--原文地址
继续我们上面的问题,结构没有构造函数为什么也能new呢?分析IL代码,发现new class的时候也就是new 引用类型的时候,IL的指令是newobj,并且还instance了相应的构造函数,new struct的时候,IL的指令却是initobj。这似乎就是问题的根源所在。
先说说newobj指令吧,执行该指令的时候,他会将我们指定的类型实例化,而且根据需要会将新类型中的成员初始化一次,再把这个对象送到托管堆上。然后再根据参数调用构造函数。构造函数对该类型做的操作就是在托管堆中进行了,最后在线程堆上只是把这个对象的引用压栈。(我们的托管堆上其实存在一个指针。该指针就是我们垃圾回收的最高检察官。关于他,咱们以后再做描述。)
那我们的initobj做了什么工作呢?MSDN上给出的解释很简单。该指令就是初始化值类型。因为编译后,非primitive type的值类型已经存在于线程堆栈上。但是还没有初始化。所以需要做这一步操作。而所有的primitive type在编译时会初始为0.这也就是为什么char和MyStruct同为值类型而char没有init的原因了。
通过上面的描述,我们应该可以清晰的看出.NET CLR中的运行过程,对象的创建是一个递归的过程。恰恰也是咱们通常所说的Builer模式的一个体现。至此,我们的“垃圾”都已经制造完毕,下一步可以讨论生成的垃圾该如何回收了。这一步最重要的理解在于初始化。
MSIL速查手册下载,感谢无私奉献版权声明:原创技术文章,如需转载,请声明出处。不得用于任何商业形式活动,否则将追究法律责任。
PS:非常感谢
sumtec@beijing,剑在上海^^,两位网友提出的意见。