CLR via C#-托管堆和垃圾回收

托管堆基础

访问类型的资源

面向对象的环境中,每个类型都代表可供程序使用的一种资源。要使用这些资源,必须为代表资源的类型分配内存。以下是访问一个资源所需的步骤。

①调用IL指令newobj,为代表资源的类型分配内存,由new操作符来完成。

②初始化内存,设置资源的初始状态并使资源可用,类型的实例构造器负责设置初始状态。

③访问类型的成员来使用资源。

④摧毁资源的状态以进行清理。

⑤释放内存,垃圾回收器独自负责这一步。

托管堆为开发人员提供了一个简化的编程模型,分配并初始化资源并直接使用。

大多数类型都无需资源清理,垃圾回收器会自动释放内存。在类中也可以调用Dispose方法立即进行垃圾回收。

从托管堆分配资源

CLR要求所有对象都从托管堆分配

进程初始化时,CLR划出一个地址空间区域作为托管堆。

NextObjPtr指针

CLR还要维护一个指针,称为NextObjPtr,这个指针指向下一个对象再堆中的分配位置。刚开始时,NextObjPtr设为地址空间区域的基地址。

一个区域被非垃圾对象填满后,CLR会分配更多的区域,这个过程一直重复,直至整个进程地址空间都被填满。

所以你的应用程序的内存受进程的虚拟地址空间的限制,32位进程最多能分配1.5GB,64位进程最多能分配8TB。

new操作符使CLR执行的工作

①计算类型的字段,包括从基类继承的字段所需的字节数。

②加上对象的开销所需的字节数。每个对象都要两个开销字段,类型对象指针和同步块索引。

③CLR检查区域中是否有分配对象所需要的字节数,如果托管堆空间足够,就在NextObjPtr指针指向的地址放入对象,为对象分配的字节会被清零。

④接着调用类型的实例构造器,为this参数传递NextObjPtr,new操作符返回对象引用。返回之前,NextObjPtr指针的值会加上对象占用的字节数来得到一个新值,即下个对象放入托管堆时的地址。

下图展示了包含三个对象的一个托管堆,如果要分配新对象,他将放在NextObjPtr指针指向的位置,紧接在对象C后。


垃圾回收算法

引用计数法

许多系统采用了引用计数法来管理对象的生存期,堆上的每个对象都维护着一个内存字段来统计程序中多少部分在使用对象。

如果有些部分不再需要的对象,就递减对象的计数字段。计数字段变成0,对象就可以从内存中删除了。

但是面对互相持有的情况,计数器就会永远不为0。鉴于引用计数垃圾回收算法存在的问题,CLR改为使用一种引用跟踪法。

引用跟踪法

引用跟踪算法只关心引用类型的变量,因为只有这种变量才能引用堆上的对象,值类型变量直接包含值类型实例。我们将所有的引用类型的变量都称为根。

①暂停所有线程

垃圾回收时首先暂停进程中所有线程,防止检查期间访问对象并改变其状态。

②垃圾回收标记阶段

CLR遍历堆中所有对象,将同步块索引字段中的一位设为0,表示此对象应删除。

③CLR检查所有活动根

查看他们引用了哪些对象。如果一个根包含null,CLR忽略这个根并继续检查下个根。

任何根如果引用了堆上的对象,CLR都会标记那个对象,将该对象的同步块索引中的位设为1。

一个对象被标记后,CLR会再检查那个对象中的根,标记他们引用的对象。

如果发现对象已经被标记,就不重新检查对象的字段,避免了因为循环引用而产生死循环。

检查完毕后堆中对象已标记的对象不能被垃圾回收,这种对象被称为可达的,未标记的对象是不可达的。

④垃圾回收压缩阶段

CLR将堆中已标记的对象压缩,使它们占用连续的内存空间。

压缩后,根引用的还是对象最初在内存中的位置,所以CLR还要从每个根减去所引用的对象在内存中偏移的字节数。

包装根引用的还是之前的对象,只是对象在内存中换了位置。

⑤移动NextObjPtr指针

压缩结束后NextObjPtr指针指向最后一个幸存对象之后的位置。

下一个对象将分配在这个位置。

⑥CLR恢复应用程序的所有线程

如果CLR在一次GC之后回收不了内存,而且进程中没有空间来分配新的GC区域,就说明该进程的内存已耗尽。


CLR的垃圾回收是基于代的垃圾回收器,并遵循以下原则

①对象越新,生存期越短。

②对象越老,生存期越长。

③回收堆的一部分,速度快于回收整个堆。

代的原理

①托管堆在初始化时不包含对象,添加到堆的对象称为第0代对象,也就是那些新构造的对象,GC从未检查过他们。

下图展示一个新启动的应用程序,他分配了五个对象,运行一段时间后,对象C和E变得不可达。

CLR初始化时位第0代对象选择一个预算容量,以KB位单位。如果分配一个新对象造成第0代超过预算就必须启动一次垃圾回收。

②假设A到E刚好用完第0代的空间,那么分配对象F就必须启动垃圾回收。

垃圾回收器判断C和E是垃圾,压缩D使之与B相邻,在垃圾回收中存活的对象B和D现在成为第1代对象。

一次垃圾回收后第0代就不包含任何对象了,新对象会分配到第0代中。

③程序继续运行,分配了对象F到K,运行一段时间后B,H和J变得不可达。

④假定现在分配新对象L会造成第0代超出预算,必须进行垃圾回收。垃圾回收器根据预算容量检查代。

对象越新生存期越短。因此第0代包含更多垃圾的可能性更大,能回收更多的内存。

选择忽略第1代中的对象,可以加快垃圾回收速度,不必遍历托管堆中的每个对象。

如果根或对象引用了老一代的某个对象,垃圾回收器就可以忽略老对象内部的所有引用,能在更短的时间内构造好可达对象图

为了确保对老对象的已更新字段进行检查,垃圾回收器在对象的引用字段发生变化时,会设置一个对应的位标志。

这样就知道自上次垃圾回收以来那些老对象的字段是否发生变化,对变化的老对象检查是否引用了第0代中的任何对象。

所有幸存的第0代对象都成为了第1代的一部分。即使对象B已经不可达,但也没有被垃圾回收。

⑤假设应用程序继续运行,分配对象L到O,此外停止使用对象G、L和M使他们不可达。

⑥假设分配对象P导致第0代超过预算,垃圾回收发生。

第1代中所有的对象占据的内存仍小于预算所以垃圾回收器再次决定只回收第0代。

⑦第1代正在缓慢增长,假定第1代的增长导致他的所有对象占用了全部预算。

这时应用程序继续运行,并分配对象P到S,使第0代对象达到他的预算容量。

⑧应用程序试图分配对象T时,由于第0代已经满了,所以必须开始垃圾回收。

但这一次垃圾回收器发现第1代占用了太多内存,以至于用完了预算。

所以垃圾回收器觉得需要检查第1代和第0代中的所有对象。

和之前一样,第0代的幸存者被提升到第1代,第1代的幸存者被提升至第2代,第0代再次空出来了。

托管堆只支持三代

CLR初始化会为每一代选择预算,但是CLR的垃圾回收是自调节的,垃圾回收器会在执行GC是了解应用程序的行为。

假如应用程序构造了许多对象,但每个对象用的时间都很短,此时就会对第0代垃圾回收。

如果垃圾回收器发现在回收第0代后存活下来的对象很少,就可能减少第0代的预算。

分配空间的减少意味着垃圾回收更加频繁,但垃圾回收器每次工作量也小了。

例如第0代中所有对象都是垃圾,只需让NextObjPtr指针回到第0代起始处即可。

同样的,垃圾回收器发现在回收第0代后存活下来的对象很多,就可能增加第0代的预算。这对第1代和第2代同样适用。


垃圾回收触发条件

①代码显式调用Syste.GC的静态Collect方法

②Windows报告低内存情况

③CLR正在写卸载AppDomain

④CLR正在关闭,进程终止


大对象

CLR将对象分为大对象和小对象。85000字节以上的对象是大对象。

①大对象不是在小对象的地址空间分配,而是在进程地址空间的其他地方分配。

②目前的GC不压缩大对象,在内存中移动他们代价过高。

③大对象总是第2代,不可能是第1代或第0代。所以只能为需要长时间存活的资源创建大对象。

大对象一般是大字符串,比如XML、JSON或者I/O操作的字节数组。


垃圾回收模式

CLR启动时会选择一个GC模式,进程中之前不会改变。

①工作站,针对客户端优化,GC延时低。

②服务器,针对服务器端应用程序。

应用程序默认以工作站GC模式运行。

原文地址:https://www.cnblogs.com/errornull/p/10049675.html

时间: 2024-07-31 00:39:24

CLR via C#-托管堆和垃圾回收的相关文章

重温CLR(十五) 托管堆和垃圾回收

本章要讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存.简单地说,本章要解释clr中的垃圾回收期是如何工作的,还要解释相关的性能问题.另外,本章讨论了如何设计应用程序来最有效地使用内存. 托管堆基础 每个程序都要使用这样或那样的资源,包括文件.内存缓冲区.屏幕空间.网络连接.数据库资源等.事实上,在面向对象的环境中,每个类型都代表可提供程序使用的一种资源.要使用这些资源,必须为代表资源的类型分配内存.以下是访问一个资源所需的步骤 1 调用IL指令newo

.NET 托管堆和垃圾回收

托管堆基础 简述:每个程序都要使用这样或那样的资源,包括文件.内存缓冲区.屏幕空间.网络连接.....事实上,在面向对象的环境中,每个类型都代表可供程序使用的一种资源.要使用这些资源,必须为代表资源的类型分配内存.以下是访问一个资源所需步骤:1.调用IL指令newobj,为代表资源的类型分配内存.(C# new操作符)2.初始化内存,设置资源的初始状态.(一般指构造函数)3.访问类型的成员来使用资源.(使用成员变量.方法.属性等)4.摧毁资源的状态以进行清除.(???Dispose???)5.释

【C#进阶系列】20 托管堆和垃圾回收

托管堆基础 一般创建一个对象就是通过调用IL指令newobj分配内存,然后初始化内存,也就是实例构造器时做这个事. 然后在使用完对象后,摧毁资源的状态以进行清理,然后由垃圾回收器来释放内存. 托管堆除了能避免错误使用已经被释放的内存,也会减少内存泄漏,大多数类型都无需资源清理,垃圾回收器会自动释放资源. 当然也有需要立即清理的,比如一些包含了本机资源的类型(如文件.套接字和数据库连接等),可在这些类中调用一个Dispose方法.(当然有的类对这个方法封装了一下,可能是别的名字比如断开数据库连接的

.NET的堆和栈04,对托管和非托管资源的垃圾回收以及内存分配

在" .NET的堆和栈01,基本概念.值类型内存分配"中,了解了"堆"和"栈"的基本概念,以及值类型的内存分配.我们知道:当执行一个方法的时候,值类型实例会在"栈"上分配内存,而引用类型实例会在"堆"上分配内存,当方法执行完毕,"栈"上的实例由操作系统自动释放,"堆"上的实例由.NET Framework的GC进行回收. 在" .NET的堆和栈02,值类型和

C# 托管堆和垃圾回收器GC

这里我们讨论的两个东西:托管堆和垃圾回收器,前者是负责创建对象并控制这些对象的生存周期,后者负责回收这些对象. 一.托管堆分配资源 CLR要求所有的对象都从托管堆分配.进程初始化时,CLR划出一个地址空间区域作为托管堆.CLR还要维护一个指针P,该指针指向下一个对象在堆中的分配位置. 那么我们进一步深入看看创建一个对象(也就是new 一个对象)时CLR做了哪些工作呢. 1.计算类型字段需要的字节数. 2.加上对象开销所需要的字节数,每个对象都有两个开销:类型对象指针和同步块索引 3.CLR检查区

.Net程序的内存管理和垃圾回收机制

.NET 内存管理和垃圾回收 C/C++ 程序需要开发者手动分配和释放内存,.Net程序则使用垃圾回收技术自动收集不再使用的内存.垃圾回收器(GC)使用引用 跟踪占用内存的对象,如果对象被设置为null或已不在使用范围,GC就会标志该对象为可回收,这样GC就可以回收被这些对象占用的内存. 垃圾回收器(GC)使用Win32? VirtualAlloc() 接口为自己的堆分配内存,.Net托管堆是一个巨大连续的虚拟内存.GC先预留虚拟内存,当托管堆增长时则提交内存.GC跟踪托管堆末尾可用的地址并把下

Clr Via C#读书笔记---垃圾回收机制

#1 垃圾回收平台的基本工作原理: 访问一个资源所需的具体步骤: 1)调用IL指令newobj,为代表资源的类型分配内存.在C#中使用new操作符,编译器就会自动生成该指令.2)初始化内存,设置资源的初始状态,使资源可用.类型的实例构造器负责设置该初始状态.3)访问类型的成员(可根据需要反复)来使用资源.4)摧毁资源的状态以进行清理.正确清理资源的代码要放在Finalize, Dispose和Close方法.5)释放内存.垃圾回收器独自负责这一步. 托管堆如何知道应用程序不再用一个对象? 托管堆

[CLR via C#]21. 自动内存管理(垃圾回收机制)

目录 理解垃圾回收平台的基本工作原理 垃圾回收算法 垃圾回收与调试 使用终结操作来释放本地资源 对托管资源使用终结操作 是什么导致Finalize方法被调用 终结操作揭秘 Dispose模式:强制对象清理资源 使用实现了Dispose模式的类型 C#的using语句 手动监视和控制对象的生存期 对象复活 代 线程劫持 大对象 一.理解垃圾回收平台的基本工作原理 值类型(含所有枚举类型).集合类型.String.Attribute.Delegate和Event所代表的资源无需执行特殊的清理操作.

CLR 垃圾回收算法

c#相较于c,c++而言,在内存管理上为程序员提供了极大的方便,解放了程序员与内存地址打交道,提高了程序员的工作效率.比如c中分配的malloc堆空间没有释放导致的内存泄露,数组越界导致的踩内存错误,使用了已释放的内存空间错误等等.这些在C#中统统的都不存在,主要是由于clr提供的安全检查机制以及垃圾回收机制.本篇文章主要来介绍常用的垃圾回收算法以及CLR中使用的垃圾回收算法. 在通常的情况下当分配对象时发现内存堆空间不足时,此时GC会执行垃圾回收算法.默认情况下,进程启动,会被分配相应的堆空间