官方QQ群:127876820【C#编程技术-全国站--未满人】

C#高级编程:后台内存管理

 

如果很好地理解了内存管理和C#提供的指针功能,也就能很好地集成C#代码和原来的代码,并能在非常注重性能的系统中高效地处理内存。

本章的主要内容如下:

运行库如何在堆栈和堆上分配空间

垃圾收集的工作原理

如何使用析构函数和System.IDisposable接口来确保正确释放未托管的资源

C#中使用指针的语法

如何使用指针实现基于堆栈的高性能数组

7.1 后台内存管理

C#编程的一个优点是程序员不需要担心具体的内存管理,尤其是垃圾收集器会处理所有的内存清理工作。用户可以得到像C++语言那样的效率,而不需要考虑像在C++中那样内存管理工作的复杂性。虽然不必手工管理内存,但如果要编写高效的代码,就仍需理解后台发生的事情。本节要介绍给变量分配内存时计算机内存中发生的情况。

注意:

本节的许多内容是没有经过事实证明的。您应把这一节看作是一般规则的简化向导,而不是实现的确切说明。

7.1.1 值数据类型

Windows使用一个系统:虚拟寻址系统,该系统把程序可用的内存地址映射到硬件内存中的实际地址上,这些任务完全由Windows在后台管理,其实际结果是32位处理器上的每个进程都可以使用4GB的内存—— 无论计算机上有多少硬盘空间。(在64位处理器上,这个数字会更大)。这个4GB内存实际上包含了程序的所有部分,包括可执行代码、代码加载的所有DLL,以及程序运行时使用的所有变量的内容。这个4GB内存称为虚拟地址空间,或虚拟内存,为了方便起见,本章将它简称为内存。

4GB中的每个存储单元都是从0开始往上排序的。要访问存储在内存的某个空间中的一个值,就需要提供表示该存储单元的数字。在任何复杂的高级语言中,例如C#、VB、C++和Java,编译器负责把人们可以理解的变量名称转换为处理器可以理解的内存地址。

在进程的虚拟内存中,有一个区域称为堆栈。堆栈存储不是对象成员的值数据类型。另外,在调用一个方法时,也使用堆栈存储传递给方法的所有参数的复本。为了理解堆栈的工作原理,需要注意在C#中变量的作用域。如果变量a在变量b之前进入作用域,b就会先出作用域。下面的代码:

{

int a;

// do something

{

int b;

// do something else

}

}

首先声明a。在内部的代码块中声明了b。然后内部的代码块终止,b就出作用域,最后a出作用域。所以b的生存期会完全包含在a的生存期中。在释放变量时,其顺序总是与给它们分配内存的顺序相反,这就是堆栈的工作方式。

我们不知道堆栈在地址空间的什么地方,这些信息在进行C#开发是不需要知道的。堆栈指针(操作系统维护的一个变量) 表示堆栈中下一个自由空间的地址。程序第一次运行时,堆栈指针指向为堆栈保留的内存块末尾。堆栈实际上是向下填充的,即从高内存地址向低内存地址填充。当数据入栈后,堆栈指针就会随之调整,以始终指向下一个自由空间。这种情况如图11-1所示。在该图中,显示了堆栈指针800000(十六进制的0xC3500),下一个自由空间是地址799999。

图 11-1

下面的代码会告诉编译器,需要一些存储单元以存储一个整数和一个双精度浮点数,这些存储单元会分别分配给nRacingCars和engineSize,声明每个变量的代码表示开始请求访问这个变量,闭合花括号表示这两个变量出作用域的地方。

{

int nRacingCars = 10;

double engineSize = 3000.0;

// do calculations;

}

假定使用如图11-1所示的堆栈。变量nRacingCars进入作用域,赋值为10,这个值放在存储单元799996~799999上,这4个字节就在堆栈指针所指空间的下面。有4个字节是因为存储int要使用4个字节。为了容纳该int,应从堆栈指针中减去4,所以它现在指向位置799996,即下一个自由空间 (799995)。

下一行代码声明变量engineSize(这是一个double),把它初始化为3000.0。double要占用8个字节,所以值3000.0占据栈上的存储单元799988~799995上,堆栈指针减去8,再次指向堆栈上的下一个自由空间。

当engineSize出作用域时,计算机就知道不再需要这个变量了。因为变量的生存期总是嵌套的,当engineSize在作用域中时,无论发生什么情况,都可以保证堆栈指针总是会指向存储engineSize的空间。为了从内存中删除这个变量,应给堆栈指针递增8,现在指向engineSize使用过的空间。此处就是放置闭合花括号的地方。当nRacingCars也出作用域时,堆栈指针就再次递增4,此时如果内存中又放入另一个变量,从799999开始的存储单元就会被覆盖,这些空间以前是存储nRacingCars的。

如果编译器遇到像int i、j这样的代码,则这两个变量进入作用域的顺序就是不确定的:两个变量是同时声明的,也是同时出作用域的。此时,变量以什么顺序从内存中删除就不重要了。编译器在内部会确保先放在内存中的那个变量后删除,这样就能保证该规则不会与变量的生存期冲突。

7.1.2 引用数据类型

堆栈有非常高的性能,但对于所有的变量来说还是不太灵活。变量的生存期必须嵌套,在许多情况下,这种要求都过于苛刻。通常我们希望使用一个方法分配内存,来存储一些数据,并在方法退出后的很长一段时间内数据仍是可以使用的。只要是用new运算符来请求存储空间,就存在这种可能性——例如所有的引用类型。此时就要使用托管堆。

如果以前编写过需要管理低级内存的C++代码,就会很熟悉堆(heap)。托管堆和C++使用的堆不同,它在垃圾收集器的控制下工作,与传统的堆相比有很显著的性能优势。

托管堆(简称为堆)是进程的可用4GB中的另一个内存区域。要了解堆的工作原理和如何为引用数据类型分配内存,看看下面的代码:

void DoWork()

{

Customer arabel;

arabel = new Customer();

Customer otherCustomer2 = new EnhancedCustomer();

}

在这段代码中,假定存在两个类Customer 和 EnhancedCustomer。EnhancedCustomer类扩展了Customer类。

首先,声明一个Customer引用arabel,在堆栈上给这个引用分配存储空间,但这仅是一个引用,而不是实际的Customer对象。arabel引用占用4个字节的空间,包含了存储Customer对象的地址(需要4个字节把内存地址表示为0到4GB之间的一个整数值)。

然后看下一行代码:

arabel = new Customer();

这行代码完成了以下操作:首先,分配堆上的内存,以存储Customer实例(一个真正的实例,不只是一个地址)。然后把变量arab

posted @ 2010-09-22 13:40  碧海蓝天_C#  阅读(613)  评论(0编辑  收藏  举报
官方QQ群:127876820【C#编程技术-全国站--未满人】