最近开坛子里问内存的人挺多,写了篇破文,有兴趣的看下吧

cuike519 2009-12-23 04:19:43
.NET内存分配浅析
我知道这是一个富有神话色彩的主题,同样也是个深奥的主题,说它神话是因为.NET程序员几乎看不到它,但是它一直在保护着.NET程序的运行,说它深奥可能涉及一些底层的东西在这个高级的编程语言里显得有点与众不同。我希望通过本文能和大家一起分享.NET关于内存分配上的一些经验,正如题目所描述这里只是浅析,因为我的知识也大部分来自MSDN和一些观察的结果。

一个有趣的假设
.NET很霸道,.NET程序基于这样一个假设,用户的内存是无限的(这怎么可能呢),为了管理这个“无限”的内存.NET需要一个管理器来在有限的内存上模拟出来一个无限的内存空间,对于.NET应用程序来说这些都是透明的(应用程序是看不到的),.Net程序只管贪婪的申请内存,其他事情就有这个管理器来处理,这个管理器微软叫它垃圾收集器(这个概念在JAVA里面早就有了)。基于这个“无限”内存的假设.NET的内存分配是线性的,线性分配内存是最高效的,在分配内存的时候首先计算需要分配的地址空间然后再将头指针偏移即可,这时候头指针指向下一次要分配的内存的起始位置。如果内存真的是无限的,可以想象这种程序的运行将会多么高效,可惜的是内存是有限的,神话结束了。

内存分类
说到内存要简单提一下现代计算机中普遍存在的两种类型的内存 --- 栈和堆。
栈是一种数据结构,这种数据结构是计算机的核心数据结构所以该结构在CPU本身已经支持,什么是栈?栈是一种后进先出的数据结构,它只能在末端进行插入和删除元素的操作,所有的函数调用都是通过栈完成的,由于程序员不需要自己来维护栈上面的内存分配任务,所以栈内存又叫自动内存,所谓自动内存就是它的分配和释放完全由系统自动管理。不幸的是栈的空间是有限的,所以容纳的对象也是有限的,Windows操作系统上默认栈的大小是1M。

堆内存是操作系统实现的一种动态内存管理方法,在Windows中有Win32堆、CRT堆、托管堆等,在.NET应用程序里面使用的就是托管堆。这里简单的描述一下Window堆管理器的概念以及工作方法,Windows有一个堆管理器专门处理对内存的分配,堆管理器是虚拟内存管理器的消费者,堆管理器始终从虚拟内存管理器中获得新的内存区域,堆管理器将这片内存初始化为堆内存供应用程序使用。堆管理器分两部分:前端分配和后端分配。申请内存总是从前端分配开始,如果前端分配不能满足需求则会启用后端分配,前端分配会将内存分成若干大小不同的块(内存页面字节倍数),这些块被按照大小散列到一个包含128个项的链表中(Windows中称之为Look Aside List),众所周知散列表是查找最快的一种数据结构,这是快速分配内存的基础,假如应用程序要分配18个字节的内存,则堆管理器首先将18+8(这8个字节是堆管理器用来管理内存的元数据描述)=26字节,那么堆管理器会按照此方法来找26/8-1=2。堆管理器会在第二个槽中查找是否有可用的内存,如果有则将该内存返回给应用程序,并且堆管理器会将这个槽标示为已被使用(会有一个数据结构专门处理这个标记,这里从略)如果没有则在第三个槽上(临近的二倍的内存槽)查找是否有空闲的内存,如果找到了空闲的内存则将该空闲内存一分为二,将其中一个返回给应用程序,将另一个放入第二个槽中备用,如果还没有则向Windows虚拟内存管理器提出申请,申请新的堆段。为了提高效率,操作系统还提供一个内存已经分配的快照列表,该列表有0或1组成,如果是0则说明对应的槽上没有内存可用,如果为1则说明该槽上有内存可用,这个内部的存储结构有操作系统维护,开发人员不用关心。
上面简单的描述了操作系统中的两种内存结构以及操作系统如何管理这些内存,其中有关堆管理的描述不完整,只是大概的描述,有兴趣的朋友可以参看MSDN相关文档的描述。

托管堆
托管堆是由Windows的堆管理器分配的一块内存。这部分堆实际上可以理解为自动内存的衍生,在C++时代,内存的管理完全由程序员自己控制,这种完全控制导致粗心的程序员总是会分配了内存而忘记释放内存,导致各种各样的异常(OOM只是其中的一个表现形式)。在.NET时代微软为了将开发人员从内存管理中解脱出来专心处理业务,微软实现了托管堆,托管堆顾名思义是由另外一个管理器来管理的内存。
分配内存必然需要释放内存,否则内存总是会被耗尽,那么托管堆是如何分配和释放内存的?.NET应用程序开始运行CLR会为该应用程序创建一个默认的托管堆,应用程序的所有的内存分配都在该堆上进行,如果堆段被耗尽,则CLR向操作系统申请一个较大的堆段,一般为两倍,如果没有两倍的堆段可以分配,则将申请减半,如果还不行则再减半,直到申请被减小到堆段的最小的阀值,就会出现OOM。
如何分配内存?托管堆上的内存被分为3代:0代、1代、2代。用户的内存分配永远在0代上。1代,2代可以理解为系统维护的一个缓冲区,老对象总是慢慢升级到高一级的代上,那么2代中的对象是在程序生命周期中最老的对象集合。另外为了提高效率托管堆上有一个独立的堆叫:大对象堆。大对象堆用来放置尺寸超过85K的对象,大对象堆也是由多个堆段组成,大对象堆的垃圾回收策略和2代一样,当2代发生垃圾会收时,大对象堆上也需要进行垃圾回收。高一级的代被回收时会触发低一级代的回收,也就是说当发生2代回收时0,1代也会被回收,大对象堆也会被回收。前面说了托管堆的分配是线性的,这一点和Win32堆不同,线性意味着O(1)的时间复杂度,效率是最高的,这也是为什么会说.NET程序运行起来会比较快的原因(别拍砖,请往下看)。
如何回收内存?要说清楚这个问题需要知道一个概念,什么是垃圾,垃圾就是在地址空间中不再被任何对象引用的对象(孤立的对象?),要判断什么是垃圾就要知道什么是根,根是判断对象是否是垃圾的唯一标准,常见的根有静态变量、全局变量、寄存器变量、函数调用栈上的变量。寄存器变量和函数调用栈上的变量从函数调用的角度来说本质是相同的,当函数发生调用时,参数被压入栈,有些参数被分配给寄存器(依赖于编译器),寄存器中的对象(引用)是当前线程正在使用的对象所以视为根。全局和静态的变量伴随应用程序的整个生命周期所以它们也是根。知道什么是根了以后就需要如何判断对象和这些根有引用关系,首先GC会建立一个根列表,这个根列表表示当前有多少根,垃圾收集器首先会将所有的对象都看作是垃圾,然后开始逐个遍历这些对象的引用,如果最终能找到这个对象和根之间的引用关系则标记这个对象不是垃圾,否则是垃圾,当遍历完所有的根之后,所有的对象都只有两个状态:1、是垃圾。2、不是垃圾。此时GC开始回收所有垃圾对象所占用的内存空间,前面说到内存分配是线性的,所以垃圾对象被清除之后必然会在这个线性的结构上产生很多空的“洞”。这些“洞”显然是可以再次利用的内存,并且这些洞会造成大量的内存碎片,为了解决这个问题GC启动一个叫做“压缩”的机制,该机制将移动托管堆上的所有对象让他们靠拢到一起,填充这些空“洞”,此时GC还需要调整每个对象的引用(这些对象的地址都发生了变化)。到此为止垃圾回收结束。需要说明的一点是:垃圾回收开始时所有的工作线程都处于挂起状态,直到垃圾回收结束。这里只描述了一个普通的过程,GC是一个复杂的管理器,其中有很多其他的内容,比如用来调用析构函数的对象列表。这些内容本文不作详细描述,有兴趣的可以参考MSDN的相关文档。正是由于这个GC的回收机制可能会导致系统性能下降,这也就是为什么.NET程序运行起来会比较慢的原因。但是需要说明的是垃圾回收的时间是不可预期的,当它触发某些条件时才会触发垃圾回收,触发这些条件的时刻是不固定的。
何时垃圾回收?当满足下面几点时会发生垃圾回收:1、当在托管堆上的0代上分配内存被耗尽时(刚才说了,所有的用户对象都被分配在0代上),或者分配一个大对象超过了大对象堆的阀值。2、当显示调用GC.Collect()的时候,该函数有多个重载版本,具体内容参考MSDN。3、当操作系统的内存比较紧张时,这个是由操作系统发送通知给垃圾收集器的一条消息。当有上面三种情况之一发生时垃圾收集器开始工作,其中第一种情况是最长发生的。

技巧
在诊断.NET内存问题的时候首先应该了解上面的知识,上面的知识只是一个概括性的描述,其中细节很多,但是基本上能够满足一般程序分析的要求。
下面两篇文章介绍了几个简单的工具和技巧:
《性能分析摘要》
http://blog.csdn.net/cuike519/archive/2009/12/14/5004245.aspx
《作为.NET开发者你必须熟悉的几个工具》
http://blog.csdn.net/cuike519/archive/2009/12/11/4983719.aspx

总结
上面简单的描述了.NET在内存分配上的一些内容,在社区里几乎每天都有人在问:我的程序内存如何如何大,找不到原因,本文也算是对基础概念的一个回顾吧。我是个懒人,本来想画几张图加深理解,最后也懒得画了,希望我描述的还算清楚。文中难免有差错,如果有请及时和我联系,欢迎大家讨论交流,此文同时发布到blog和论坛中。
...全文
211 24 打赏 收藏 转发到动态 举报
写回复
用AI写文章
24 条回复
切换为时间正序
请发表友善的回复…
发表回复
xupeihuagudulei1 2010-03-07
  • 打赏
  • 举报
回复
此文章对操作系统的知识要求比较大了
像我这种从来没有学过操作系统的
看起来比较吃力。
antony1029 2009-12-31
  • 打赏
  • 举报
回复
垃圾回收机制
.NET框架包含一个托管堆,所有的.NET语言在分配引用类型对象时都要使用它。像值类型这样的轻量级对象始终分配在栈中,但是所有的类实例和数组都被生成在一个内存池中,这个内存池就是托管堆。
.NET框架中的垃圾回收器被称为分代的垃圾回收器(Generational Garbage Collector),也就是说被分配的对象划分为3个类别,或称为“代”。分别为0,1,2。0、1、2代对应的托管堆的初始化大小分别是256K,2M和10M。垃圾回收器在发现改变大小能够提高性能的话,会改变托管堆的大小。例如当应用程序初始化了许多小的对象,并且这些对象会被很快回收的话,垃圾回收器就会将第0代的托管堆变为128K,并且提高回收的频率。如果情况相反,垃圾回收器发现在第0代的托管堆中不能回收很多空间时,就会增加托管堆的大小。在应用程序初始化的之前,所有等级的托管堆都是空的。当对象被初始化的时候,他们会按照初始化的先后顺序被放入第0代的托管堆中。 

最近被分配内存空间的对象被放置于第0代,因为第0代很小,小到足以放进处理器的二级(L2)缓存,所以第0代能够为我们提供对其中对象的快速存取。经过一轮垃圾回收后,仍然保留在第0代中的对象被移进第1代中,再经过一轮垃圾内存回收后,仍然保留在第1代中的对象则被移进第2代中。第2代包含了生存期较长的对象,这些对象至少经过了两轮回收。

C#程序为一个对象分配内存时,托管堆几乎可以立即返回新对象所需的内存,托管堆之所以能有这样高效的内存分配性能是由于托管堆较为简单的数据结构。托管堆类似于简单的字节数组,有一个指向第一个可用内存空间的指针。

在某块被某对象所请求时,上述指针值就会返回给调用函数,而指针会重新调整至指向下一个可用的内存空间。分配一个托管内存块只比递增一个指针的值稍微复杂一点。这也是托管堆所优化的性能之一。在一个不需太多垃圾回收的应用程序中,托管堆的表现会优于传统的堆。

由于这个线性的内存分配方法的存在,在C#应用程序中同时分配的对象在托管堆上通常会被分配成彼此相邻。着安排和传统的堆内存分配完全不同,传统的堆内存分配是基于内存块大小的。例如,两个同时分配的对象在堆上的位置可能相距很远,从而降低了缓存的性能。因此虽然内存分配很快,但在一些比较重要的程序中,第0代中的可用内存很有可能会彻底被消耗光。记住,第0代小到可以装进L2缓冲区,并且没有被使用的内存不会被自动释放。当第0代中没有可以分配的有效内存时,就会在第0代中触发一轮垃圾回收,在这轮垃圾回收中将删除所有不再被引用的对象,并将当前正在使用中的对象移至第1代。针对第0代的垃圾回收是最常见的回收类型,而且速度很快。在第0代的垃圾内存回收不能有效的请求到充足的内存时,就启动第1代的垃圾内存回收。第2代的垃圾内存回收要作为最后一种手段而使用,当且仅当第1代和第0代的垃圾内存回收不能被提供足够内存时进行。如果各代都进行了垃圾回收后仍没有可用的内存,就会引发一个OutOfMemeryException异常 。

CopperBell 2009-12-30
  • 打赏
  • 举报
回复
up
xiaodemingaa 2009-12-30
  • 打赏
  • 举报
回复
学习了 顶你了!
数据之巅 2009-12-28
  • 打赏
  • 举报
回复
好贴,谢谢楼主分享。。。。
Alden 2009-12-28
  • 打赏
  • 举报
回复
有点意思
castlooo 2009-12-23
  • 打赏
  • 举报
回复
搬个 板凳
vip__888 2009-12-23
  • 打赏
  • 举报
回复
占位招租
tan124 2009-12-23
  • 打赏
  • 举报
回复
占个位置
jbo126 2009-12-23
  • 打赏
  • 举报
回复
俺崇拜你,崇拜得不行不行滴~
wiki14 2009-12-23
  • 打赏
  • 举报
回复
支持
wuyq11 2009-12-23
  • 打赏
  • 举报
回复
支持
gqqnb 2009-12-23
  • 打赏
  • 举报
回复
谢谢楼主介绍
zhouyanfss 2009-12-23
  • 打赏
  • 举报
回复
很好,牛人
yuquanzuo 2009-12-23
  • 打赏
  • 举报
回复
顶一下
V68V6 2009-12-23
  • 打赏
  • 举报
回复


三年不鸣
dyss 2009-12-23
  • 打赏
  • 举报
回复
mark
mxc1225 2009-12-23
  • 打赏
  • 举报
回复
不错!收藏之!
zqtoo 2009-12-23
  • 打赏
  • 举报
回复
支持 顶
十八道胡同 2009-12-23
  • 打赏
  • 举报
回复
支持
加载更多回复(2)

17,740

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 .NET Framework
社区管理员
  • .NET Framework社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧