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++中这个是不存在的,所有的对象实体都要代码明确得释放,否则会一直占据内存空间。

我们知道引用类型,维护的是一个指针。这个指针本身实际上也是放在堆栈(Stack)中的,而指向的内存空间是在堆(heap)中。GC(垃圾收集)去释放堆中的内存是根据引用指针来判断地,如果堆中的一个内存块已经没有任何引用(引用变量已经超出范围),那么当一轮垃圾回收进行时,这块内存会被释放。事实上.Net的垃圾回收机制并不只是做了这些。通常堆中的数据,由于多次释放和分配,会变得支离破碎。成为一块块的内存块,这样为分配新的内存空间造成麻烦。因为分配新的空间时需要遍历整个堆空间,以确定一块足够创建对象实例的内存块,并建立引用变量,同时维护堆的存储记录。这样不但效率底下,而且会降低内存使用率。

GC在垃圾回收时会做两个步骤,1.释放内存空间。2.重新调整并压缩堆区,使剩下的对象重新移动到堆的一个端部。尽管这样做会造成一些额外的资源开销,但是完成这个步骤后,实例化一个对象,以及查找访问实例都会快很多,事实上提高了性能。这个第二步机制也是托管堆和未托管堆的主要区别。

垃圾收集的时机是由系统控制的,具体的算法微软并没有公布过。通常我们也可以自己在需要的时候调用垃圾回收机制,能够显式地执行System.GC.Collect()。但不推荐,因为这个过程事实上是比较消耗资源的,没有自动收集效率高;对整个程序的执行效果来说,正常情况下还是使用系统的自动收集机制更好些。

下一篇文章将会详细分析垃圾回收机制和C#的对象使用。

posted @ 2011-06-25 03:41  Rocky Yang  阅读(2386)  评论(2编辑  收藏  举报