托管堆基础

一般创建一个对象就是通过调用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 托管堆和垃圾回收的更多相关文章

  1. CLR via C# 读书笔记-21.托管堆和垃圾回收

    前言 近段时间工作需要用到了这块知识,遂加急补了一下基础,CLR中这一章节反复看了好多遍,得知一二,便记录下来,给自己做一个学习记录,也希望不对地方能够得到补充指点. 1,.托管代码和非托管代码的区别 ...

  2. .NET 托管堆和垃圾回收

       托管堆基础 简述:每个程序都要使用这样或那样的资源,包括文件.内存缓冲区.屏幕空间.网络连接.....事实上,在面向对象的环境中,每个类型都代表可供程序使用的一种资源.要使用这些资源,必须为代表 ...

  3. 【CLR】解析CLR的托管堆和垃圾回收

    目录结构: contents structure [+] 为什么使用托管堆 从托管堆中分配资源 托管堆中的垃圾回收 垃圾回收算法 代 垃圾回收模式 垃圾回收触发条件 强制垃圾回收 监视内存 对包装了本 ...

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

    本章要讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存.简单地说,本章要解释clr中的垃圾回收期是如何工作的,还要解释相关的性能问题.另外,本章讨论了如何设计 ...

  5. 托管堆和垃圾回收(GC)

    一.基础 首先,为了深入了解垃圾回收(GC),我们要了解一些基础知识: CLR:Common Language Runtime,即公共语言运行时,是一个可由多种面向CLR的编程语言使用的"运 ...

  6. cir from c# 托管堆和垃圾回收

    1,托管堆基础 调用IL的newobj 为资源分配内存 初始化内存,设置其初始状态并使资源可用.类型的实列构造器负责设置初始化状态 访问类型的成员来使用资源 摧毁状态进行清理 释放内存//垃圾回收期负 ...

  7. C#托管堆和垃圾回收

    垃圾回收 值类型 每次使用都有对应新的线程栈 用完自动释放 引用类型 全局公用一个堆 因此需要垃圾回收 操作系统 内存是链式分配 CLR 内存连续分配(数组) 要求所有对象从 托管堆分配 GC 触发条 ...

  8. 如何管好.net的内存(托管堆和垃圾回收)

    一:C#标准Dispose模式的实现 需要明确一下C#程序(或者说.NET)中的资源.简单的说来,C#中的每一个类型都代表一种资源,而资源又分为两类: 托管资源:由CLR管理分配和释放的资源,即由CL ...

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

    在" .NET的堆和栈01,基本概念.值类型内存分配"中,了解了"堆"和"栈"的基本概念,以及值类型的内存分配.我们知道:当执行一个方法的时 ...

随机推荐

  1. Senparc.Weixin.MP SDK 微信公众平台开发教程(九):自定义菜单接口说明

    上一篇<Senparc.Weixin.MP SDK 微信公众平台开发教程(八):通用接口说明>介绍了如何通过通用接口获取AccessToken,有了AccessToken,我们就可以来操作 ...

  2. tomcat项目无法发布异常,Could not copy all resources to .........(转)

    [plain] <span style="font-size:18px;">Deployment failure on Tomcat  6.x. Could not c ...

  3. 爱上MVC~为Html.EditorForModel自定义模版

    回到目录 挺有意思的一件事 对于MVC视图渲染来说,大家应该不会陌生,但对于模型的渲染,不知道是否听说过,主要是说Model通过它属性的相关特性(DataType,UIHint)来将它们自动渲染到Vi ...

  4. 1121冬至!!!巩固HTML基础第一堂

    今天只是把以前的知识巩固了一下.温故而知新,说的一点没错: 又新明白了一种居中对齐方法: 水平居中:align left(左侧对齐),center(居中对齐) 垂直居中:ralign top(上对齐) ...

  5. [Linux]Linux下安装和配置solr/tomcat/IK分词器 详细实例二.

    为了更好的排版, 所以将IK分词器的安装重启了一篇博文,  大家可以接上solr的安装一同查看.[Linux]Linux下安装和配置solr/tomcat/IK分词器 详细实例一: http://ww ...

  6. Atitit dsl对于数组的处理以及main函数的参数赋值

    Atitit dsl对于数组的处理以及main函数的参数赋值 1.1. 词法解析..添加了[] 方括号的解析支持1 1.2. Ast建立.添加了数组参数的支持..使用了递归下降法..getparam ...

  7. c#设计模式-观察者模式

    Observer 与 Subject 互为耦合,但是这种耦合的双方都依赖于抽象,而不依赖于具体. 一.观察者模式 目的 我们都知道解决一个问题有N种解决方式,但在面向对象的设计中如何能做到“高内聚,低 ...

  8. java之内部类详解

    序言 有位小同学要我写一篇这个的总结,我说那好吧,那就动手写总结一下这个内部类的知识,感觉这个在面试中也会经常遇到,内部类.反射.集合.IO流.异常.多线程.泛型这些重要的基础知识大家都比较容易记不住 ...

  9. ssh(sturts2_spring_hibernate) 框架搭建之struts2

    一.struts2完整流程的逻辑(整体的概述) 首先,用户在地址栏中输入你的项目访问路径,然后这个请求会发送到服务器,之后服务器发现在web.xml中配置了一个filter过滤器,并且这个过滤器需要对 ...

  10. HTTP与AJAX深入揭秘,不使用AJAX实现页面无刷新

    AJAX的原理是什么? 实际上就是发起HTTP请求,既然就是发起HTTP请求,那只要我们能够实现发起HTTP请求就可以在不使用AJAX的情况下实现相同的效果. 在前端有好多方式可以发起HTTP请求,比 ...