【C#进阶系列】21 托管堆和垃圾回收
托管堆基础
一般创建一个对象就是通过调用IL指令newobj分配内存,然后初始化内存,也就是实例构造器时做这个事。
然后在使用完对象后,摧毁资源的状态以进行清理,然后由垃圾回收器来释放内存。
托管堆除了能避免错误使用已经被释放的内存,也会减少内存泄漏,大多数类型都无需资源清理,垃圾回收器会自动释放资源。
当然也有需要立即清理的,比如一些包含了本机资源的类型(如文件、套接字和数据库连接等),可在这些类中调用一个Dispose方法。(当然有的类对这个方法封装了一下,可能是别的名字比如断开数据库连接的close)
在托管堆上分配资源
CLR要求所有对象都从托管堆分配。
进程初始化时,CLR划出一个地址空间区域作为托管堆。CLR还维护一个指针,即NextObjPtr。该指针指向下一个对象在堆中的分配未知。
一个区域被非垃圾对象填满后,CLR会分配更多的区域,这个过程不断重复,直到整个进程地址空间都被填满。32位进程最多分配1.5G,64位进程最多分配8TB。
当创建一个对象时,首先会计算该对象类型字段(包括基类)所需字节数,加上对象的开销所需的字节数(即类型对象指针和同步块索引)。
然后CLR会检查区域中是否有分配对象所需字节数大小的内存。如果托管堆有,那么就在NextObjPtr指向的地址放入对象,且NextObjPtr会加上对象占用的字节数得到新值,即下个对象放入时的地址。
通过垃圾回收器(GC)回收资源
CLR在创建对象时发现没有足够内存分配该对象,那么就会执行垃圾回收。
CLR在进行垃圾回收时,首先会暂停所有线程,
标记阶段:然后CLR会遍历堆中所有的引用对象,将同步块索引字段中的一位设为0,表示所有对象都要删除。然后检查所有的活动根(即所有引用类型的字段以及方法的参数和局部变量),查看它们引用了哪些对象。任何根引用了堆上的对象,那么CLR就标记那个对象,将同步块索引字段中的位设为1,如果对象已经被标记为1了,那么就不再重新检查对象的字段。标记为1的也就是被引用的对象,称为可达的,标记为0的就是不可达的。此时CLR就知道了哪些对象可以删除,哪些对象不能删除。
压缩阶段:CLR对堆中已标记的对象进行搬移内存位置(且对象所有的根的引用也自然会跟着变动),使得被标记的对象紧密相连,即占用连续的内存空间。这样不仅减小了应用程序的工作集,从而提升了访问性能,还得到了大量的未占用内存空间,并且解决了内存碎片化的问题。
最后,恢复所有线程。
静态字段引用的对象一直存在,直到用于加载类型的AppDomain卸载为止。内存泄漏的一个常见原因就是让静态字段引用某个集合对象,然后不停地往集合添加数据项。静态字段使集合对象一直存活,而集合对象使所有数据项一直存活。因此应该尽量避免使用静态字段。(或者参照前面的玩法,当我们不用静态变量的时候,可以立马置为null,那么垃圾就会被回收)。
有一个神奇的垃圾回收特例——Timer。原因是它会每隔一段时间去调用回调函数,但是根据之前学的垃圾回收玩法可以知道当Timer的变量离开了作用域,且没有其它函数引用了Timer对象,那么在垃圾回收时Timer就会被回收掉。也就不会去执行回调函数了。(所以说慎用Timer,这里有这么一个大坑)
代:提升性能
CLR的GC是基于代的垃圾回收器。它对代码做了如下假设:
- 对象越新,生存期越短
- 对象越老,生存期越长
- 回收堆的一部分,速度快于回收整个堆
第0代:新添加到堆的对象称为第0代对象,垃圾回收器从未检查过它们。
第1代:第0代对象经过一次垃圾回收,但是并没有被当做垃圾释放掉,那么就会在压缩阶段一起放入第1代对象区域。
第2代:第1代对象又经过了一次垃圾回收,但是并没有被当做垃圾释放掉,那么就会在压缩阶段一起放入第2代对象区域。没有第3代,第2代放着的就是经过了2次和2次以上垃圾回收的对象。
第0代内存区域满了就会进行垃圾回收,此时不仅会回收第0代的区域,还会去判断第1代区域是否也满了,满了也回收第1代,不满的话即时第1代里面有不可达的对象,那么也不会回收第1代。
CLR初始化时,会为这三代分别选择内存预算,以此判断什么时候该回收了。但是CLR的垃圾回收器是自调节的。
也就是说
如果垃圾回收器发现第0代回收后存活下来的对象很少,那么就会减少第0代的预算,这样的话垃圾回收就会发生得更频繁了,然而垃圾回收器每次做的事更少了,这减小了工作集。如果没有一个存活的话,连压缩都免了。
如果垃圾回收器发现第0代回收后存活下来的对象很多,那么就会加大第0代的预算,这样的话垃圾回收就会发生得不频繁了,然而垃圾回收器每次回收的内存要多得多。(如果没有回收到足够的内存,那么垃圾回收器会执行一次完整回收,如果还是没有足够内存,那么就会抛出OutOfMemoryException异常)。
上面是用第0代举例,第1、2代也如是。
垃圾回收触发条件
CLR在检测第0代超过预算时会触发一次GC,这是GC最常见的触发条件,还有其它的触发如下:
- 代码显示调用System.GC的静态Collect方法
- Windows报告低内存情况
- CLR正在卸载AppDomain
- CLR正在关闭
大对象
CLR将对象分为大对象和小对象,以85000字节为界限。
大对象不是在小对象的地址空间分配,而是在进程地址空间的其它地方分配。
目前版本的GC不压缩大对象,因为在内存中移动它们代价过高。(可能会造成空间碎片)
大对象总是第2代,所以只能为需要长时间存活的资源生成大对象,否则若短时间存活的大对象放在第二代中,因为之前讲到一次回收过多内存,就会将代的预算减少,导致更频繁回收第2代,会损害性能。
垃圾回收模式
CLR启动时会选择一个GC模式,进程终止前该模式不会改变:
- 工作站模式
- 该模式针对客户端应用程序优化GC。GC造成延时很低,应用程序线程挂起时间很短,避免使用户感到焦虑。在该模式下,GC假定机器上运行的其它应用程序都不会消耗太多的CPU资源。
- 服务器模式
- 该模式针对服务器端应用程序优化GC。被优化的主要是吞吐量和资源利用。GC假定机器上没有运行其它应用程序,并假定机器上所有的CPU都可以用来辅助完成GC。该模式造成托管堆被分为几个区域(section),每个CPU一个。开始垃圾回收时,垃圾回收器在每个CPU上都运行一个特殊线程,每个线程和其他线程并发回收它自己的区域。对于工作者线程(worker thread)行为一致的服务器应用程序,并发回收能很好地进行。这个功能要求应用程序在多CPU计算机上运行,使线程能真正地同时工作吗,从而获得性能上的提升。
应用程序默认以工作站GC模式运行。寄宿了CLR的服务器应用程序(比如ASP.NET和Sql Server)可请求CLR加载“服务器”GC,但如果是单处理器计算机上运行,CLR将总是使用工作站GC模式。
独立应用程序可在配置文件中,加上下面配置项告诉CLR使用服务器模式:
<configuration>
<runtime>
<gcServer enabled="true"/>
</runtime>
</configuration>
除了这两种模式,GC还支持两种子模式:并发(默认)和非并发。
在并发模式下,GC有一个额外线程,能在运行时并发标记对象。
而由另一个线程去判断是否压缩对象,GC可以更倾向于决定不压缩,有利于增强性能,但会增大应用程序工作集。使用并发垃圾回收器,消耗的内存比非并发更多。
加上以下配置项告诉CLR使用非并发模式:
<configuration>
<runtime>
<gcConcurrent enabled="false"/>
</runtime>
</configuration>
使用需要特殊清理的类型
大多数类型只需要内存就可以了,然而有的类型还需要本机资源。比如System.IO.FileStream类型需要打开一个文件(本机资源)并保存文件的句柄。
包含本机资源的类型被GC时,GC会回收对象在托管堆中使用的内存。但这样会造成本机资源(GC对它一无所知)的泄漏,所以CLR提供了称为终结的机制,允许对象在被判定为垃圾之后,但在对象内存被回收之前执行一些代码。
任何包装了本机资源(文件,网络连接,套接字,互斥体)的类型都支持终结。CLR判定一个对象不可达时,对象将终结它自己,释放它包装的本机资源。之后GC会从托管堆回收对象。
C#的语法,跟析构函数差不多,但是所代表的意义不同
public class Troy {
~Troy() {
//这里的代码就是垃圾回收前执行的代码,这段代码会被放在一个try块中,而finally部分放的是base.Finalize
}
}
这个语法最后在IL代码里还是生成一个叫Finalize的方法。
被视为垃圾的对象在垃圾回收完毕后才调用Finalize方法,所以这些对象的内存不是马上被回收,因为Finalize方法可能要执行访问字段的代码。
可终结对象在回收时必须存活,造成它被提升到另一代,使对象活得比正常时间长。这增大了内存耗用,所以应尽量避免终结。
终结的内部原理
在创建新对象的时候,会在堆中分配内存。如果对象的类型定义了Finalize方法,那么在该类型的实例构造器被调用之前,会将指向该对象的指针放入到一个终结列表里。
终结列表是一个由垃圾回收器控制的内部数据结构,列表的每一项都指向一个个对象——回收该对象的内存前应调用它的Finalize方法。
在每次要回收垃圾对象时标记阶段走完都会去扫描终结列表,如果存在垃圾对象的引用,该引用被移除终结列表,并附加到freachable队列。(此时对象将不再被认为是垃圾,不能回收其内存,被称为对象复活了)
freachable队列也是垃圾回收器的一个内部数据结构,队列中的每个引用所指向的对象都已经准备好调用Finalize方法了。
CLR用一个特殊的、高优先级的专用线程调用Finalize方法来避免死锁。
如果freachable队列为空,那么此线程睡眠,一旦不为空,此线程会被唤醒,将每一项都从队列中移除,并且同时调用每个对象的Finalize方法。
然后进入压缩阶段,将这些复活的对象提升到下一代。
然后清空freachable队列,并执行每个对象的Finalize方法。
到了下次执行垃圾回收时,因为终结列表已经没有这些对象的指针了,所以现在它们被认为是真正的垃圾了,也就会被释放。
整个过程中,执行了两次垃圾回收才释放掉内存,在实际的过程中,由于对象可能被提升至另一代,所以可能要求不止进行两次垃圾回收。
手动监视和控制对象的生存期
CLR为每个AppDomain都提供了一个GC句柄表,允许应用程序监视和手动控制对象的生存期。这个就太6了,感觉用不到,用得到的时候回来再看吧。
PS:
最近两章效率真是慢,一方面因为双休没看书和一些突发状况,另一方面也是因为已经开始了CLR的核心机制之旅,里面的很多东西确实没听过,感觉难度开始增大了。
在此过程中键盘莫名其妙坏了,并且两次关机废了写了一半的博客。今天才发现原来强行关机后再开机,浏览器中写了一半的博客是可以恢复的。
【C#进阶系列】21 托管堆和垃圾回收的更多相关文章
- CLR via C# 读书笔记-21.托管堆和垃圾回收
前言 近段时间工作需要用到了这块知识,遂加急补了一下基础,CLR中这一章节反复看了好多遍,得知一二,便记录下来,给自己做一个学习记录,也希望不对地方能够得到补充指点. 1,.托管代码和非托管代码的区别 ...
- .NET 托管堆和垃圾回收
托管堆基础 简述:每个程序都要使用这样或那样的资源,包括文件.内存缓冲区.屏幕空间.网络连接.....事实上,在面向对象的环境中,每个类型都代表可供程序使用的一种资源.要使用这些资源,必须为代表 ...
- 【CLR】解析CLR的托管堆和垃圾回收
目录结构: contents structure [+] 为什么使用托管堆 从托管堆中分配资源 托管堆中的垃圾回收 垃圾回收算法 代 垃圾回收模式 垃圾回收触发条件 强制垃圾回收 监视内存 对包装了本 ...
- 重温CLR(十五) 托管堆和垃圾回收
本章要讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存.简单地说,本章要解释clr中的垃圾回收期是如何工作的,还要解释相关的性能问题.另外,本章讨论了如何设计 ...
- 托管堆和垃圾回收(GC)
一.基础 首先,为了深入了解垃圾回收(GC),我们要了解一些基础知识: CLR:Common Language Runtime,即公共语言运行时,是一个可由多种面向CLR的编程语言使用的"运 ...
- cir from c# 托管堆和垃圾回收
1,托管堆基础 调用IL的newobj 为资源分配内存 初始化内存,设置其初始状态并使资源可用.类型的实列构造器负责设置初始化状态 访问类型的成员来使用资源 摧毁状态进行清理 释放内存//垃圾回收期负 ...
- C#托管堆和垃圾回收
垃圾回收 值类型 每次使用都有对应新的线程栈 用完自动释放 引用类型 全局公用一个堆 因此需要垃圾回收 操作系统 内存是链式分配 CLR 内存连续分配(数组) 要求所有对象从 托管堆分配 GC 触发条 ...
- 如何管好.net的内存(托管堆和垃圾回收)
一:C#标准Dispose模式的实现 需要明确一下C#程序(或者说.NET)中的资源.简单的说来,C#中的每一个类型都代表一种资源,而资源又分为两类: 托管资源:由CLR管理分配和释放的资源,即由CL ...
- NET的堆和栈04,对托管和非托管资源的垃圾回收以及内存分配
在" .NET的堆和栈01,基本概念.值类型内存分配"中,了解了"堆"和"栈"的基本概念,以及值类型的内存分配.我们知道:当执行一个方法的时 ...
随机推荐
- Senparc.Weixin.MP SDK 微信公众平台开发教程(九):自定义菜单接口说明
上一篇<Senparc.Weixin.MP SDK 微信公众平台开发教程(八):通用接口说明>介绍了如何通过通用接口获取AccessToken,有了AccessToken,我们就可以来操作 ...
- tomcat项目无法发布异常,Could not copy all resources to .........(转)
[plain] <span style="font-size:18px;">Deployment failure on Tomcat 6.x. Could not c ...
- 爱上MVC~为Html.EditorForModel自定义模版
回到目录 挺有意思的一件事 对于MVC视图渲染来说,大家应该不会陌生,但对于模型的渲染,不知道是否听说过,主要是说Model通过它属性的相关特性(DataType,UIHint)来将它们自动渲染到Vi ...
- 1121冬至!!!巩固HTML基础第一堂
今天只是把以前的知识巩固了一下.温故而知新,说的一点没错: 又新明白了一种居中对齐方法: 水平居中:align left(左侧对齐),center(居中对齐) 垂直居中:ralign top(上对齐) ...
- [Linux]Linux下安装和配置solr/tomcat/IK分词器 详细实例二.
为了更好的排版, 所以将IK分词器的安装重启了一篇博文, 大家可以接上solr的安装一同查看.[Linux]Linux下安装和配置solr/tomcat/IK分词器 详细实例一: http://ww ...
- Atitit dsl对于数组的处理以及main函数的参数赋值
Atitit dsl对于数组的处理以及main函数的参数赋值 1.1. 词法解析..添加了[] 方括号的解析支持1 1.2. Ast建立.添加了数组参数的支持..使用了递归下降法..getparam ...
- c#设计模式-观察者模式
Observer 与 Subject 互为耦合,但是这种耦合的双方都依赖于抽象,而不依赖于具体. 一.观察者模式 目的 我们都知道解决一个问题有N种解决方式,但在面向对象的设计中如何能做到“高内聚,低 ...
- java之内部类详解
序言 有位小同学要我写一篇这个的总结,我说那好吧,那就动手写总结一下这个内部类的知识,感觉这个在面试中也会经常遇到,内部类.反射.集合.IO流.异常.多线程.泛型这些重要的基础知识大家都比较容易记不住 ...
- ssh(sturts2_spring_hibernate) 框架搭建之struts2
一.struts2完整流程的逻辑(整体的概述) 首先,用户在地址栏中输入你的项目访问路径,然后这个请求会发送到服务器,之后服务器发现在web.xml中配置了一个filter过滤器,并且这个过滤器需要对 ...
- HTTP与AJAX深入揭秘,不使用AJAX实现页面无刷新
AJAX的原理是什么? 实际上就是发起HTTP请求,既然就是发起HTTP请求,那只要我们能够实现发起HTTP请求就可以在不使用AJAX的情况下实现相同的效果. 在前端有好多方式可以发起HTTP请求,比 ...