调式内存问题
概要:本章节描述了怎么样动手调试内存消耗问题,内存的问题可能在使用asp.net应用程序时有些经历。首先,讲讲在dotnet中,内存是如何管理的;然后讲讲垃圾收集器(GC)是如何回收没有使用的内存的。最后是一个从头到尾的内存消耗的调试过程。
dotnet内存管理和垃圾收集:
传统的c 和 c++程序容易产生内存泄露问题,因为开发人员必须手动的分配和释放内存。在MS dotnet 平台中,手动的释放内存是不必需的,因为垃圾收集机制会自动地回收没有用的内存,这使得内存使用更加的安全和有效。
垃圾收集器(GC)使用一种引用的机制来跟踪内存中的对象。当一个对象被设置成null 或不再在生存器内,GC把这个对象标示为可回收的。GC会收回在操作系统中被这个对象占用的内存块。
GC的性能优点体现在延迟的对象收集和能立刻回收很大数量的对象。相比于传统的windows操作系统的内存管理方式,GC 会尽可能多的去使用内存。长沙电脑维修网为广大电脑爱好者提供电脑维修入门知识!
dotnet中的垃圾收集机制使用 WIN32 VirtualAlloc() 应用程序接口(API)来申请一块内存作为他的堆(heap),一个dotnet 托管堆是一个很大的,连续的虚拟内存区域。GC首先申请一块虚拟内存,把它当作托管的堆,GC会跟踪在托管堆末尾的可用内存地址,并把下一步将要分配的内存放在该区域。所以,所有的dotnet内存都一块一块的被放在了托管堆中,这种方式巨大的改良了分配时间,因为它省去了像一般的分配方式从内存自由列表或内存块列表中搜索一个合适大小的自由块,随着时间的流逝,对象被删除,内存碎片形成了,好像一个个小漏洞,当垃圾收集发生时,GC整理堆,把这些小洞堵上,通过移动内存块,移动的方式是内存直接拷贝。下图显示了如何工作:
代 (Generations)
GC 改善内存管理性能是通过把对象分割到基于时间的代中去,代的概念就仿佛是 爷爷 父亲 和儿子 这种三代的关系。当收集行为发生时,在最年轻的代上的对象被收集,如果这样不足以释放出足够的可用内存,一个接一个的,更加老一点的代会被收集,使用代的作用,在于在任何一个时间,GC只是和一个已经分配的对象的子集一起工作。在一个时间点上,并不是所有的对象都是需要的。
当许多对象收集时,对象在代之间移动对象,老的对像会向更老的代移动。
随着时间推移,更老的代会充满更老的对象,代越老,就应该更稳定,所需要的收集也就越少。
当某个代的内存极限达到时,对这个代的收集行为就发生了。在dotnet 1。0版的程序实现中,对 第0 代 ,第一代 和第二代的内存回收的阀值是256kb,2M和10M,不同的,要注意,GC可以动态的调整这些阀值根据一个应用程序回收设计的不同。一般的,大对象(比85K大的认为是大对象)会自动地被放到大对象堆(LOH)中。长沙电脑维修网为广大电脑爱好者提供电脑维修入门知识!
根:
GC使用引用机制来决定在一个托管堆中一个特定的内存块是否可以被回收。和其他的GC实现机制不一样,并不是给每一个已经分配的块设置一个堆标记来决定这个块是不是可以被回收。针对每个应用程序,GC生成一棵引用树,来跟踪应用程序中的每个对象。
根引用树
CG 将一个对象放置到根引用树中,该对象必须至少有一个父对象来引用它。每一个应用程序有一个根集合,该集合中包含全局对象,静态对象 及和此有关的进程栈和动态实例化的对象。开始一次收集前,GC从根开始,向下建立一个可见的引用的树,GC建立一个核心的活动对象的列表,然后在托管堆中寻找那些没有在这个活动列表中的对象,进行回收。
这种检查一个对象是不是活动的方法也许会被认为代价是比较昂贵的,相对于在内存中使用简单的标记或使用引用计数器。但这种方法能准确地完成回收。引用计数器可能会错误的大于或小于引用数。一个堆标记也会错误的把一个在内存中仍然活动的对象标记为删除。dotne托管堆避免了这种问题,它通过查找每一个活动的对象,然后建立一个引用列表,然后开始收集。但,这种方法还要解决循环的内存引用的问题。
如果有一个引用到某一个活动对象,而且这个对象是被强烈要求放到根活动列表的。dotnet 提供了一个方法叫弱根对象引用。这种方法为程序员提供了方法,来告诉GC,他们想要能访问这个对象,但他们不要妨碍这个对象被收集,因此这个对象就是可见的,存在的,直到GC来收集它们,弱引用行为就类似于缓存cache 这种行为。比如说,你有申请了一个大对象,但该对象不是完全的被删除或被回收,你能够拿到该对象,并重用它,当然这种情况是在没有内存压力,没有清除托管堆的时候。
大对象堆(Large Object Heap,LOH)
dotnet内存管理方式把所有分配大于或等于85,000bytes 的内存块放到单独得堆中,该堆成为 大对象堆。这个堆由主托管堆中一系列独立的虚拟内存块组成。在托管堆中使用一个独立的堆来存放大对象是因为垃圾收集需要移动内存块 ,移动大对象的代价是比较大的,而且大对象也不能压缩了,所以弄大对象是你必须要考虑的一件事情。
举个例子来说明:当你申请1MB的内存作为单独块,LOH 扩展到1MB ,当你释放这个对象,LOH不会放弃掉这个1MB,它仍然保持着1MB,如果你再申请500KB,这个新的块会被放在属于LOH1MB的空间里面。在进程的生命周期中,LOH总是保持增长来存放当前被引用的大对象,当对象被释放后也不会收缩。即使GC过来收集垃圾,也是不释放的。