- 本博客所总结书籍为《CLR via C#(第4版)》清华大学出版社,2021年11月第11次印刷(如果是旧版书籍或者pdf可能会出现书页对不上的情况)
- 你可以理解为本博客为该书的精简子集,给正在学习中的人提供一个“glance”,以及对于部分专业术语或知识点给出解释/博客链接。
- 【本博客有如下定义“Px x”,第一个代表书中的页数,第二个代表大致内容从本页第几段开始。(如果有last x代表倒数第几段,last代表最后一段)】
- 电子书可以在博客首页的文档-资源归档中找到,或者点击:传送门自行查找。如有能力请支持正版。(很推荐放在竖屏上阅读本电子书,这多是一件美事)
- 欢迎加群学习交流:637959304 进群密码:(CSGO的拆包密码)
本章末尾处部分原理不作深入探讨,这里不作展开记录,有兴趣可以自行阅读。
目录
- 第二十一章 托管堆和垃圾回收
- 托管堆基础
- 代:提升性能
- 使用需要特殊清理的类型
第二十一章 托管堆和垃圾回收
托管堆基础
- 在面向对象环境中,每个类型代表可供程序使用的一种资源,使用资源要为代表资源的类型分配内存。步骤如下:(P447 2) 1、调用IL指令newobj,为代表资源的类型分配内存(一般使用C# new操作符来完成)。 2、初始化内存,设置资源的初始状态并使资源可用。类型的实例构造器负责设置初始状态。 3、访问类型的成员来使用资源(有必要可以重复)。 4、摧毁资源的状态以进行清理。 5、释放内存。垃圾回收器独自负责这一步。
- CLR要求所有对象都从托管堆分配。进程初始化时,CLR划出一个地址空间区域作为托管堆。CLR还要维护一个指针,我把它称作NextObjPtr。该指针指向下一个对象在堆中的分配位置。刚开始的时候,NextObjPtr设为地址空间区域的基地址。(P488 4) 一个区域被非垃圾对象填满后,CLR会分配更多的区域。这个过程会一直重复,知道整个进程地址空间都被填满。(P448 5)
- C# 执行new操作符时的CLR步骤:(P448 last2) 1、计算类型的字段(以及从基类型继承的字段)所需的字节数。 2、加上对象的开销所需的字节数。 3、CLR检查区域中是否有分配对象所需的字节数。
- 托管堆在内存中连续分配对象,所以会因为引用的“局部化”(locality)而获得性能上的提升。具体地说,这意味着进程的工作集会非常小,应用程序只需使用很少的内存,从而提高了速度。还意味着代码使用的对象可以全部驻留在CPU的缓存中。结果是应用程序能以惊人的速度访问这些对象,因为CPU在执行大多数操作时,不会因为“缓存未命中”(cache miss)而被迫访问较慢的RAM。(P449 1)
- 应用程序调用new操作符创建对象时,可能没有足够地址空间来分配该对象。发现空间不够,CLR就执行垃圾回收。(P449 3)
- 垃圾回收算法-引用计数:组件对象模型中。堆上的每个对象都维护着一个内存字段来统计程序中多少“部分”正在使用对象。随着每一“部分”到达代码中某个不再需要对象的地方,就递减对象的计数字段。计数字段变成0时对象就可以从内存中删除了。(P449 last2)
- 垃圾回收算法-CLR使用引用跟踪算法:引用跟踪算法只关心引用类型的变量,因为只有这种变量才能引用堆上的对象;值类型变量直接包含值类型实例。我们将所有引用类型的变量都成为根。(P449 last)
- CLR开始GC时,首先暂停进程中的所有线程。这样可以防止线程在CLR检查期间访问对象并更改其状态。然后,CLR进入GC的标记阶段。在这个阶段,CLR遍历堆中的所有对象,将同步块索引字段中的一位设为0。这表明所有对象都应删除。然后,CLR检查所有活动根,查看它们引用了哪些对象。这正是CLR的GC称为引用跟踪GC的原因。如果一个根包含null,CLR忽略这个根并继续检查下个根。 任何根如果引用了堆上的对象,CLR都会标记那个对象,也就是将该对象的同步块索引中的位设为1。一个对象被标记后,CLR会检查那个对象中的根,标记它们引用的对象。如果发现对象已经标记,就不重新检查对象的字段。这就避免了因为循环引用而产生死循环。(P450 1)
回收前的托管堆
- CLR 知道哪些对象可以幸存,哪些可以删除后,就进入GC的压缩(compact)阶段。在这个阶段,CLR对堆中已标记的对象进行“乾坤大挪移”,压缩所有幸存下来的对象,使它们占用连续的内存空间。这样做有许多好处。首先,所有幸存对象在内存中紧挨在一起,恢复了引用的“局部化”,减小了应用程序的工作集,从而提升了将来访问这些对象时的性能。其实,可用空间也全部是连续的,所以这个地址空间区段得到了解放,允许其他东进驻。最后,压缩意味着托管堆解决了本机(原生)堆的空间碎片化问题。(P450 last)进行压缩后的根内存位置发生了偏移,所以CLR还要计算偏移字节数以确保能正常访问留下来的根。
垃圾回收后的托管堆
代:提升性能
- CLR的GC是基于代的垃圾回收器(generational garbage collector),它对你的代码做出了以下几点假设:(P454 1) 1、对象越新,生存期越短。 2、对象越老,生存期越长。 3、回收堆的一部分,速度快于回收整个堆。
- 托管堆在初始化时不包含对象。添加到堆的对象称为第0代对象。简单地说,第0代对象就是那些新构造的对象,垃圾回收器从未检查过它们。下图展示了一个新启动的应用程序,它分配了5个对象(从A到E)。过了一会儿,对象C和E变得不可达。(P454 – P457)
- CLR初始化时为第0代对象选择一个预算容量(以KB为单位)。如果分配一个新对象造成第0代超过预算,就必须启动一次垃圾回收。假设对象A到E刚好用完第0代的空间,那么分配对象F就必须启动垃圾回收。垃圾回收器判断对象C和E是垃圾,所以会压缩对象D,使之与对象B相邻。在垃圾回收中存活的对象(A,B和 D)现在成为第1代对象。第1代对象已经经历了垃圾回收器的一次检查。
- 一次垃圾回收后,第0代就不包含任何对象了。和前面一样,新对象会分配到第0代中。应用程序继续运行,并新分配了对象F到对象K。另外,随着应用程序继续运行,对象B,H和J变得不可达,它们的内存将在某一时刻回收。
- 现在,假定分配新对象L会造成第0代超出预算,造成必须启动垃圾回收。开始垃圾回收时,垃圾回收器必须决定检查哪些代。前面说过,CLR初始化时会为第0代对象选择预算。事实上,它还必须为第1代选择预算。
- 开始一次垃圾回收时,垃圾回收器还会检查第1代占用了多少内存。在本例中,由于第1代占用的内存远少于预算,所以垃圾回收器只检查第0代中的对象。回顾一下基于代的垃圾回收器做出的假设。第一个假设是越新的对象活得越短。因此,第0代包含更多垃圾的可能性很大,能回收更多的内存。由于忽略了第1代中的对象,所以加快了垃圾回收速度。
- 基于代的垃圾回收器还假设越老的对象活得越长。也就是说,第1代对象在应用程序中很有可能是继续可达的。如果垃圾回收器检查第Ⅰ代中的对象,很有可能找不到多少垃圾,结果是回收不了多少内存。因此,对第1代进行垃圾回收很可能是浪费时间。如果真的有垃圾在第1代中,它将留在那里。此时的堆如下图所示。
- 如此进行往复之后。第1代正在缓慢增长。假定第1代的增长导致它的所有对象占用了全部预算。这时,应用程序继续运行(因为垃圾回收刚刚完成),并分配对象Р到对象S,使第0代对象达到它的预算容量。这时的堆如下图所示。
- 应用程序试图分配对象T时,由于第0代已满,所以必须开始垃圾回收。但这一次垃圾回收器发现第1代占用了太多内存,以至于用完了预算。由于前几次对第0代进行回收时,第1代可能已经有许多对象变得不可达(就像本例这样)。所以这次垃圾回收器决定检查第1代和第0代中的所有对象。两代都被垃圾回收后,堆的情况如下图所示。
- 托管堆只支持3代:0、1、2。(P457 2)
- 垃圾回收触发的条件:(P458 last) 1、代码显示调用System.GC的静态Collect方法。 2、Windows报告低内存情况。 3、CLR正在卸载AppDomain 4、CLR正在关闭
- 还有另一个性能提升举措值得注意。CLR将对象分为大对象和小对象。本章到目前为止说的都是小对象。目前认为85000字节或更大的对象是大对象。CLR以不同方式对待大小对象: 1、大对象不是在小对象的地址空间分配,而是在进程地址空间的其他地方分配。 2、目前版本的GC不压缩大对象,因为在内存中移动它们代价过高。 3、大对象总是第2代,绝不可能是第0代或第1代。所以只能为需要长时间存活的资源创建大对象。
- 垃圾回收模式:(P459 last) 1、工作站。该模式针对客户端应用程序优化GC。GC造成的延时很低,应用程序线程挂起时间很短,避免使用户感到焦虑。在该模式中,GC假定机器上运行的其他应用程序都不会消耗太多的CPU资源。 2、服务器。该模式针对服务器端应用程序优化GC。被优化的主要是吞吐量和资源利用。GC假定机器上没有运行其他应用程序(无论客户端还是服务器应用程序),并假定机器的所有CPU都可用来辅助完成GC。该模式造成托管堆被拆分成几个区域(section),每个CPU一个。开始垃圾回收时,垃圾回收器在每个CPU上都运行一个特殊线程;每个线程都和其他线程并发回收它自己的区域。对于工作者线程(worker thread)行为一致的服务器应用程序,并发回收能很好地进行。这个功能要求应用程序在多CPU计算机上运行,使线程能真正地同时工作,从而获得性能的提升。 除了这两种主要模式,GC还支持两种子模式:并发(默认)或非并发。
- 强制垃圾回收:System.GC类型允许应用程序对垃圾回收器进行一些直接控制。(P462 2)
- 监视应用程序的内存使用:GC类提供了一些静态方法,可以调用他们查看某一代发生了多少次垃圾回收,或者托管堆中的对象当前使用了多少内存。(P463 4)
使用需要特殊清理的类型
- 部分类型需要内存和本机资源才能进行工作。例如:System.IO.FileStream等打开关闭本机文件的操作。
- 终结机制:包含本机资源的类型被GC时,GC 会回收对象在托管堆中使用的内存。但这样会造成本机资源(GC对它一无所知)的泄漏,这当然是不允许的。所以,CLR提供了称为终结(finalization)的机制,允许对象在被判定为垃圾之后,但在对象内存被回收之前执行一些代码。任何包装了本机资源(文件、网络连接、套接字、互斥体)的类型都支持终结。CLR 判定一个对象不可达时,对象将终结它自己,释放它包装的本机资源。之后,GC会从托管堆回收对象。(P464 last2)
- SafeHandle类(P467-P475)
- 终结的内部工作原理:终结表面上很简单:创建对象,当它被回收时,它的Finalize方法得以调用。但一旦深究下去,就会发现终结的门道很多。(P479-P487)