由于C#是一种托管语言,它的垃圾回收机制(GC)是由.net平台负责的,加之C#语言并没有指针,所以我们在使用过程中极少会考虑到内存使用状况以及项目在运行过程中是如何进行内存管理的。但是,C#只是在内存管理方面对程序员隐藏了,并不代表它不涉及这些东西,甚至其内部内存管理或许比自己手动管理更加复杂。
一. 存储类型:
当我们程序执行的时候,在内存中(不同区域)分配有四种主要的类型:值类型,引用类型,指针,指令。
1) 值类型:
在C#中,继承自System.ValueType的类型被称为值类型,主要有以下几种(CLR2.0中支持类型有增加):
* bool*byte* char* decimal* double* enum* float
* int* long* sbyte* short* struct* uint* ulong* ushort
2) 引用类型:
以下是引用类型,继承自System.Object:
* class*interface* delegate* object* string
3) 指针:
在内存区中,指向一个类型的引用,通常被称为“指针”,它是受CLR( Common Language Runtime:公共语言运行时)管理,我们不能显示使用。需要注意的是,一个类型的引用即指针跟引用类型是两个完全不同的概念。指针在内存中(栈区)占一块内存区,它本身只代表一个内存地址(或者null),它所指向的另一块内存区(堆区)才是我们真正的数据或者类型。
当我们使用引用类型的时候,一般是对指针进行的操作而非引用类型对象本身。但是值类型则操作其本身。
二. 内存分区
1. 概念及作用:
1) 栈区:由编译器自动分配释放 ,存放值类型的对象本身,引用类型的引用地址(指针),静态区对象的引用地址(指针),常量区对象的引用地址(指针)等。其操作方式类似于数据结构中的栈。
2) 堆区(托管堆):用于存放引用类型对象本身。在c#中由.net平台的垃圾回收机制(GC)管理。
栈,堆都属于动态存储区,可以实现动态分配。
3) 静态区及常量区:用于存放静态类,静态成员(静态变量,静态方法),常量的对象本身。由于存在栈内的引用地址都在程序运行开始最先入栈,因此静态区和常量区内的对象的生命周期会持续到程序运行结束时,届时静态区内和常量区内对象才会被释放和回收(编译器自动释放)。所以应限制使用静态类,静态成员(静态变量,静态方法),常量,否则程序负荷高。
4) 代码区:存放函数体内的二进制代码。
三. 内存分配
1) 值类型
Int a = 10;
Int 表示变量为值类型;
变量a放在代码区;
10赋值给a,10存放在a在栈区开辟的内存区内;
a内赋的值为值类型对象本身,即为值10(在栈区内存区内a代表的就是值10本身)。
2) 值类型扩展(与引用类型扩展对比)
Int a =10;
Int b =a;
b = 20;
10存放在a在栈区开辟的内存区内。a 把值10赋给b,b会在重新在栈区开辟的新的内存区来储存值,两个内存区保持独立互不影响。b重新赋值,b会把内存区内(栈区)的值10替换成值20重新存储,因为两个内存区互相独立,因此a的内存区内的值不会受影响而改变,仍然为值10。
在栈上a和b分别占用一块内存区,互不干扰。
3) 引用类型
Person Tom = new Person();
Tom存放在代码区;
new Person()产生的对象存放在堆区开辟的内存区内;
将堆区对象的引用地址赋值给Tom,引用地址(指针)存放在Tom在栈区开辟的内存区内;(在栈区内存区内Tom保存的是一份引用地址(指针),通过引用地址去堆区内寻找对象本身的值)
4) 引用类型扩展
classProgram
{
staticvoid Main(string[] args)
{
Person Tom = newPerson();
Tom.age = 20;
Person Nacy = newPerson();
//赋的值是引用地址(对指针进行操作),共同的引用地址指向堆区的同一个内存区的值
Nacy = Tom;
//改变了堆区内存区的值
Nacy.age = 30;
//堆区内存区的值改变后,影响所有指针指向的结果
int newAge = Tom.age;//newAge的值为30
}
}
classPerson
{
publicint age{get;set;}
}
栈上的指针Tom和Nacy指向堆上同一个区域Person,修改其一必会改变堆上的数据。
5) c#静态成员在内存中如何分配?
C#里的 静态变量 是算值类型还是引用类型?
值类型存储在栈中, 引用类型存储在堆中. 这与静态与否没关系, 静态与否只影响对象的初始化()
所谓静态,就是一定会存在的而且会永恒存在、不会消失,这样的数据包括常量、常变量(const 变量)、静态变量、全局变量等。这些变量的指针存放在栈区,值本身存放在静态存储区,他们在程序编译完成后就已经分配好了,生命周期持续至程序结束。
6)
7)
堆和栈的对比
经典例子:
以下代码说明的很形象:
//引用类型(''class'' 类类型)
class SomeRef { public int32 x;}
//值类型(''struct'')
struct SomeVal(pulic Int32 x;}
static void ValueTypeDemo()
{
SomeRef r1=new SomeRef();//分配在托管堆
SomeVal v1=new SomeVal();//堆栈上
r1.x=5;//解析指针
v1.x=5;//在堆栈上修改
SomeRef r2=r1;//仅拷贝引用(指针)
SomeVal v2=v1;//先在堆栈上分配,然后拷贝成员
r1.x=8;//改变了r1,r2的值
v1.x=9;//改变了v1,没有改变v2
}
四. 内存管理
1) 栈区管理:
C#中栈是编译期间就分配好的内存空间,因此你的代码中必须就栈的大小有明确的定义;栈区内存无需我们管理,也不受GC管理,栈顶元素使用完毕弹出就会立即释放。
2) 堆区管理:
堆区是程序运行期间动态分配的内存空间,你可以根据程序的运行情况确定要分配的堆内存的大小。在C#中堆区内存由GC(Garbage collection:垃圾收集器)负责清理,当对象超出作用域范围或者对象失去指向的引用地址,就会在一定时间内进行统一的处理,无需程序员手动处理。
当对象不再使用时,这个被存储在堆栈中的引用变量将被删除,但是从上述机制可以看出,在托管堆中这个引用指向的对象仍然存在,其空间何时被释放取决垃圾收集器而不是引用变量失去作用域时。
在使用电脑的过程中大家可能都有过这种经验:电脑用久了以后程序运行会变得越来越慢,其中一个重要原因就是系统中存在大量内存碎片,就是因为程序反复在堆栈中创建和释入变量,久而久之可用变量在内存中将不再是连续的内存空间,为了寻址这些变量也会增加系统开销。在.net中这种情形将得到很大改善,这是因为有了垃圾收集器的工作,垃圾收集器将会压缩托管堆的内存空间,保证可用变量在一个连续的内存空间内,同时将堆栈中引用变量中的地址改为新的地址,这将会带来额外的系统开销,但是,其带来的好处将会抵消这种影响,而另外一个好处是,程序员将不再花上大量的心思在内在泄露问题上。
垃圾收集器除了会压缩托管堆、更新引用地址、还会维护托管堆的信息列表。