C#的数据类型以及内存管理机制剖析(1)
尽管C#(事实上是基于.Net Framework的所有语言)自动处理了内存的分配和释放的问题,并且引入了垃圾收集机制,有完善的数据类型管理能力。但是对于很多情况下,了解其深层的机制是非常有用的,能够大大提高程序的效率。如今Phone7的发布,在移动设备和一些特殊应用上。聪明而又有技巧地处理内存管理和各种数据类型显得非常有用,能够更好得维护和开发程序。
1. Windows内存管理机制
各位要深入了解C#的内存管理机制,首先必须要先了解下Windows的内存管理系统。俺记得大学的时候开过一门课叫《操作系统原理》,大家别砸我,这门课俺烤得不好……hh, 关键是提一下内存管理系统,大家还记得块映射和分页映射不?CPU在高速缓里维护了一张表,放的是内存和磁盘上的数据块的映射关系。具体我就不说了(书上弄了两章来掰扯这个机制),反正就是把一个程序的内存空间分成很多块,会有个算法把最近使用的内存块写入内存(RAM),内存块还可以分,放进高速缓存(Cache),以便下次访问能够高速读取。当然还涉及到写的机制,有很多种方式,还有冲突处理,锁什么的,这些机制当年很令人抓狂哈,现在想想其实网站和程序的缓存机制不是就脱胎于这些算法吗?只不过简单了很多。在32位的系统下,一个程序的最大内存空间是4GB,这4GB就这么在底层的操作系统下管理着,分成了很多段,页和块。这些内存块里放了些什么东西呢?嗯,大家都知道的,数据和逻辑。逻辑就是你的代码处理的方法,怎么去计算,运用,处理数据。那数据就是要加工的对象了,在C++里面分成简单数据类型和指针数据类型。这个数据,包括变量,常量,对象实体。通常数据占据了一个程序绝大部分的内存空间。也是我们需要进行内存管理的部分。
2. C#数据类型
其实C#中的数据类型也是和C++一样分两种,分别是值类型和引用类型。
(1)值类型对应于C++的简单数据类型,比如int,double,bool,值类型是.Net Framework的预定义类型,总共有13个,详见下表:
类型 | 说明 | 注意 |
char | 16位unicode字符 | |
sbyte | 8位有符号整型 | |
short | 16位有符号整型 | |
int | 32位有符号整型 | 数值默认变量类型 |
long | 64位有符号整型 | 前缀:L |
byte | 8位无符号整型 | |
ushort | 16位无符号整型 | |
unit | 32位无符号整型 | 前缀:U |
ulong | 64位无符号整型 | 前缀:UL |
float | 32位单精浮点数 | 前缀:F |
double | 64位双精浮点数 | 默认浮点数变量类型 |
decimal | 128位高精度十进位数 | 会造成性能损失,前缀:M |
bool | bool值 | true/false |
*16进制数加前缀0x,如0x12ba;
还有一点必须指出,C#中的结构struct和C++不同,这是一个值类型,不能被继承。struct派生于System.ValueType。
.Net的内置类型也叫CTS类型,值类型存储的位置是内存空间的堆栈区(stack),《数据结构》里的堆栈哈(先进后出)
{ int p1; int p2; //code { int q1; int q2; //code } }
stack top | 变量 |
700012-700015 | p1 |
700008-700011 | p2 |
700004-700007 | q1 |
700000-700003 | q2 |
stack bottom |
可看到上图所示,变量是外层括号先进栈,内层后进栈的顺序。释放时则反之。堆栈指针一直会指向最近进栈的变量,同时保留指向下一个堆栈空位的指针。
这样的结构会使得变量的读取非常高效,只要移动指针就能获得和释放变量。缺点是不够灵活,管理很大的数据结构效率不高。对于值类型,是不需要.net的来及回收机制的,本身就能很好得进行内存管理。变量的生存期要求嵌套,不能控制其生存周期。
(2)引用类型对应另外一种C++的数据类型,指针类型。这就是说我们如果创建一个引用类型,那么实际是在堆中分配了一块内存空间。然后这个变量实际上是指向这个内存块的指针,在C#中有两种数据是作为引用类型的:第一类是CTS类型中的引用类型,有两个object和string; 第二类是C#中各种的类,必须从object继承(包括自定义类和C#类库)。广义上讲object类型加上string也对,因为为所有C#(.Net Framwork)的类都是继承于System.object这个基类的。
为什么我们要把Object和String类型分开呢?这个是有原因的,string类型是个很特殊的引用类型。
using System; namespace StringTest { class StringExpress { public static void Main() { string s1 = "I am a string"; string s2 = s1; Console.WriteLine(s1); Console.WriteLine(s2); s2 = "I am a new string"; Console.WriteLine(s1); Console.WriteLine(s2); } } }
既然是引用类型大家觉得通常情况下应该显示什么呢?因为变量只是个引用(指针),所以s1和s2应该是指向同一个内存区的,事实上,当运行
string s2 = s1;
s1和s2的确是指向一样的字符串变量的,前两行输出是:
I am a string
I am a string
大家猜猜后两行会是什么呢?结果是:
I am a string
I am a new string
这就是string类型的特殊之处了,当你给string赋一个新值时,将会创建新的对象而不会覆盖原来的值,这实际上是应用了运算符重载,重新分配了引用对象。
Object类有很多应用和特点,会在以后的文章中讨论。装箱拆箱,还有一些对类基本方法也都是在Object中定义的。
3. 垃圾收集
垃圾回收是.Net中的一个非常重要的机制,事实上这个机制在很大程度上确保了.Net Framework的高性能。垃圾回收是一种对引用类型数据进行释放的自动机制。在C++中这个是不存在的,所有的对象实体都要代码明确得释放,否则会一直占据内存空间。
我们知道引用类型,维护的一个指针(32位系统是一个32位的指针)。这个指针实际上也是放在堆栈(Stack)的,而指向的内存空间是在堆(heap)中。GC(垃圾收集)去释放堆中的内存是根据引用指针的,如果一个堆中内存块已经没有任何引用(引用变量已经超出范围),那么当一轮垃圾回收进行时,这块内存会被释放。事实上.Net的垃圾回收机制并不只是做了这些。通常堆中的数据,由于多次释放和分配,会变得支离破碎。成为一块块的内存块,这样为分配新的内存空间造成麻烦。因为分配新的空间时需要遍历整个堆空间,以确定一块足够创建对象实例的内存块,并建立引用变量,同时维护堆的存储记录。这样不但效率底下,而且会降低内存使用率。
GC在垃圾回收时会做两个步骤,1.释放内存空间。2.重新调整压缩堆区,使其他对象重新移动到堆的一个端部。尽管这样做会造成一些额外的资源开销,但是这样新实例化一个对象,已经查找访问对象都会快很多,从而提高了性能。这个第二步的机制是托管堆和未托管堆的主要区别。
垃圾收集的时机是由系统控制的,具体的算法微软并没有公布过。通常我们也可以自己在需要的时候调用垃圾回收机制,能够显式地执行System.GC.Collect()。但不推荐,因为这个过程事实上是比较消耗资源的,没有自动收集效率高;对整个程序的执行效果来说,正常情况下还是使用系统的自动收集机制更好些。
下一篇文章将会详细分析垃圾回收机制和C#的对象使用。
作者:Rocky Yang
出处:http://www.cnblogs.com/yangjian2006/
本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。感谢各位能够访问我的博客!
Email:Rocky的邮箱
个人博客:Rocky & Sky IT技术和管理