首页 > 代码库 > 内存分析_.Net内存原理介绍

内存分析_.Net内存原理介绍

内存原理介绍

1.       .Net应用程序中的内存

1.1.Net内存类型

Windows使用一个系统:虚拟寻址系统。这个系统的作用是将程序可用的内存地址映射到硬件内存中的实际地址上。其实际结果是在32位的Windows操作系统中,每个进程都可以使用4GB的内存,当然,64位机这个数字就更大了,在这4GB的内存中存储着可执行代码、代码加载的DLL和程序运行的所有变量,这4GB的内存成为虚拟地址空间或虚拟内存。在.Net中要使用多种类型的内存,包括:堆栈、非托管堆和托管堆。

C#将数据分为2种:值数据类型和引用数据类型,值数据类型存储在堆栈中,引用类型存储在内存的托管堆中。

内存格局通常分为4个区:

全局数据区:存放全局变量,静态数据,常量;代码区:存放所有的程序代码;栈区:存放为运行而分配的局部变量,参数,返回数据,返回地址等;堆区:即自由存储区

堆栈

堆栈用于存储应用程序执行过程中的局部变量、方法参数、返回值和其他临时值。堆栈按照每个线程进行分配,并作为每个线程完成其工作的一个暂存区。垃圾收集器并不负责清理堆栈,因为为方法调用预留的堆栈会在方法返回时被自动清理。但是请注意,垃圾收集器知道堆栈上存储的对象的引用。当对象在一种方法中被实例化时,该对象的引用(32位或64位整型值,取决于平台类型)将保留在堆栈中,而对象自己却存储在托管堆中,并在变量超出范围时被垃圾收集器收集。

堆栈实际上是向下填充,即由高内存地址向低内存地址填充,堆栈的工作方式是先分配内存的变量后释放即先进后出原则,堆栈中的变量是从下向上释放,这样就保证了堆栈中先进后出的规则不与变量的生命周期起冲突,性能非常高,但是对于所有的变量来说不太灵活,而且变量的生命周期必须嵌套。

非托管堆

非托管堆存储引用类型如类、对象、并受垃圾收集器的控制和管理。用于运行时数据结构、方法表、Microsoft中间语言(MSIL)、JITed代码等。非托管代码根据对象的实例化方式将其分配在非托管堆或堆栈上。托管代码可通过调用非托管的Win32 API或实例化COM对象来直接分配非托管堆内存。CLR出于自身的数据结构和代码原因广泛地使用非托管堆。

托管堆

托管堆是用于分配托管对象的区域,同时也是垃圾收集器的域。CLR使用分代压缩垃圾收集器。垃圾收集器之所以称为分代式,是由于它将垃圾收集后保留下来的对象按生存时间进行划分,这样做有助于提高性能。所有版本的.Net Framework都采用三代分代方法:第0代、第1代、第2代(从年轻代到年老代)。垃圾收集器之所以称为压缩式,是因为它将对象重新定位于托管堆上,从而能够消除漏洞并保持可用内存的连续性。移动大型对象的开销很高,因为垃圾收集器将这些大型对象分配在独立的且不会压缩的大型对象堆上。

堆是从下往上分配,所以自由的空间都在已用空间的上面。

1.2 对象的生命周期

概述

1)  .net对象是被分配到一块叫做托管堆(managed Heap)的内存区域上

2)  New一个对象,返回的是一个指向堆上的引用,而不是真正的对象本身,这个对象保存在栈上

3)  对象管理法则:1、new后不用再管。2、如果托管堆上没有足够的内存,就会进行垃圾回收。

4)  应用程序根:根就是一个存储位置,保存着对堆上一个对象的引用,可以是以下几种类别:

(a)       全局对象的引用(虽然在C#不允许,但是CIL的确允许分配全局对象)

(b)       静态对象/静态字段的引用

(c)       应用程序的代码库的局部对象的引用

(d)       传递进一个方法的对象参数的引用

(e)       等待被终结的对象的引用

(f)        任何引用对象的CPU寄存器

工作原理

在一次垃圾回收过程中,运行库将检查托管堆上的对象,判断应用程序是否仍然可达到它们,即是否有根的。为此,CLR将建立一个对象图,代表堆上可达的每一个对象。没有在对象图中的即为被标记为终结的对象,会从内存中清除,然后,调整可达的对象在内存上的位置,组成一个无冗余并且压缩过的堆。

在堆栈中,一旦变量超出使用范围,其使用的内存空间会被其他变量重新使用,这时其空间中被存储的值将被其他变量覆盖而不复存在,但有时候我们希望这些值仍然存在,这就需要托管堆来实现。

class1 object1;

object1=new class1();

第一句定义了一个class1的引用,实质上只是在堆栈中分配一个4个字节的空间,它将存放后来实例化对象在托管堆中的地址,在windows中这需要4个字节来表示内存地址。

第二句实例化object1对象,实际上是在托管堆中开辟一个内存空间来存储类class1的一个具体的对象,假设这个对象需要36个字节,那么object1指向的实际上是在托管堆一个大小为36个字节的连续内存空间开始的地址。由此可以看出在C#编译器中为什么不允许使用未实例化的对象,因为这个对象在托管堆中还不存在,当对象不再使用时,这个被存储在堆栈中的引用变量将被删除,但是从上述机制可以看出,在托管堆中这个引用指向的对象仍然存在,其空间何时被释放取决垃圾收集器而不是引用变量失去作用域时。

工作方式

进程中每个线程都有自己的堆栈,这是一段线程创建时保留下的地址区域。我们的“栈内存”即再次,至于“堆”内存,个人认为在未用new定义时,堆应该就是未“保留”未“提交”的自由空间,new的功能是在这些自由空间中保留出一个地址范围

栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有FIFO。FIFO的特性,在编译的时候可以指定需要的Stack的大小。在编程中,例如C/C++中,所有的局部变量都是从栈中分配内存空间,实际上也不是什么分配,只是从栈顶向上用就行,在退出函数的时候,只是修改栈指针就可以把栈中的内容销毁,所以速度最快。

堆(Heap)是应用程序在运行的时候请求操作系统分配给自己内存,一般是申请/给予的过程,在C/C++分别用malloc/New请求分配Heap,用free/delete销毁内存。由于从操作系统管理的内存分配所以在分配和销毁的时候要占用时间,所以用堆的效率低的多,但是堆的好处是可以做的很大,C/C++堆分配的Heap是不初始化的。

在Java中除了简单类型(int,char等)都是在堆中分配内存,这也是程序慢的一个主要原因。但是跟C/C++不同,Java中分配的Heap内存是自动初始化的。在Java中所有的对象(包括int的wrapper Integer)都是在堆中分配,但是这个对象的引用却是在Stack中分配。也就是说在建立一个对象时从2个地方都分配内存,在Heap中分配的内存实际建立这个对象,而在Stack中分配的内存只是一个指向这个堆对象的指针(引用)而已。

1.3垃圾回收概念(GC)引入

在使用电脑的过程中大家可能都有过这种经验:电脑用久了以后程序运行会变得越来越慢,其中一个重要原因就是系统中存在大量内存碎片,就是因为程序反复在堆栈中创建和释入变量,久而久之可用变量在内存中将不再是连续的内存空间,为了寻址这些变量将会增加系统开销。在.net中这种情形将得到很大改善,这是因为垃圾收集器的工作,垃圾收集器将会压缩托管堆的内存空间,保证可用变量在一个连续的内存空间内,同时将堆栈中引用变量中的地址修改为新地址,这将会带来额外的系统开销,但是,其带来的好处将会抵消这种影响,而另外一个好处是,程序员将不再花上大量的心思在内存泄漏问题上。

说明,在C#程序中不仅仅只有引用类型的变量,仍然也存在值类型和其他托管堆不能管理的对象,例如文件句柄、网络连接和数据库连接,这些变量的释放仍需要程序员通过析构函数或IDispose接口来做。

1.4参考资料

http://www.tudou.com/home/diary_v9913437.html

http://www.cnblogs.com/skywithcloud/archive/2011/08/12/2136789.html

http://msdn.microsoft.com/zh-cn/magazine/cc163491.aspx

http://blog.csdn.net/directionofear/article/details/8034133