浅析CLR的GC(垃圾回收器)
文章目录:
- 了解托管堆和GC
- GC高效的处理方式—代
- 特殊类型的清理
- 手动监控和控制对象生命周期
1、了解托管堆和GC
在面向对象环境中,每一个类型都代表了一种资源。我们要使用这些资源,就要为这些代表资源的类型分配内存。在C#中,我们一般使用new关键字来完成。访问资源包括以下几步:
- 使用new操作符为类型分配内存(这个过程调用了IL指令newobj)
- 初始化内存,设置资源的初始状态,来让这个资源可用(类型的实力构造器负责初始化类型状态)
- 访问类型成员使用资源
- 摧毁资源状态进行清理
- 释放内存
在C#中,我们的操作时基于CLR来完成的,我们所有对象都是从托管堆对来分配内存。当进程初始化(我们的程序)时,CLR会画出一个地址空间区域来作为托管堆。同时,CLR会维护一个指针NewObjPtr,这个指针指向下一个对象在托管堆中分配的地址。当这个区域被非垃圾的对象填满后,CLR会分配更多的区域,这个过程将一直重复,直至整个进程的地址空间都被填满。32位进程的地址空间为1.5GB,64位进程为8TB。(这里顺便提一下值类型的生命周期,值类型对象分配在线程栈上,当离开作于域时,自动销毁。)
C# new 操作符,会让CLR执行以下步骤:
- 计算类型字段的所需字节数(这里的字段是所有字段,包括基类继承的)
- 添加创建对象的额外所需字节数 (每一个对象被初始化时,会创建类型对象指针和同步块索引,32位程序为8字节,64位程序16字节)
- CLR检查区域中空间是否足够,假如足够,在NewObjPtr指针位置放入对象。这时候,对象分配的字节会被清零,然后调用类型的构造器(计算字节,NewObjPtr指针会指向旧的位置加上这个字节的位置,为下一个对象分配空间时候的位置),new操作符返回对象的引用。
(例如在托管堆已经由A,B的情况下新构建了一个C)
关于GC
当程序调用new操作符创建对象时候,假如没有足够的地址空间来分配该对象,CLR就会执行垃圾回收(调用GC)。CLR采用引用跟踪算法,这种算法只关心引用类型的变量,避免了类型循环导致对象不能被回收的问题。引用类型包括类的静态和实例字段,方法的局部变量和参数。所有的引用类型被称之为根。
CLR开始执行GC时候,会暂停进程中所有线程(防止CLR执行检查期间对象的状态被更改);然后CLR遍历托管堆中的所有对象,将同步块索引中字段的一位设置为0(标识所有对象都应该删除),然后CLR检查所有活动根,这些根引用了哪些对象。
任何一个根引用了堆的对象,CLR会将对象的同步块索引的位设置位1。然后再检查对象中的跟,标记它们引用的对象。假如在遍历过程中发现对象被标记,就跳过这个对象,不再重新检查这个对象的字段,这样避免了循环引用。
应用程序中所有的活动跟都检查完毕以后,这时候堆中的对象要么被标记了(称之为可达,由活动跟在引用),要么没有被标记(相应称为不可达)。CLR将不可达的对象内存回收。将可达对象进行内存整理,使对象内存在托管堆中是连续的(压缩过程中CLR要从每个根减去所引用对象在堆中的偏移字节数),如下图所示:
2、GC高效的处理方式—代
CLR的垃圾回收基于代。
同时GC回收垃圾时,做出了下面几点假设(可以先记下,从下文中体会):
- 对象越新,生存期越短
- 对象越来,生存期越长
- 回收堆一部分内存,比回收整个堆要快
解释下代:托管进程中有两种内存堆,分别是本机堆和托管堆,CLR在托管堆上面为.net 的所有对象分配内存(托管堆又称为GC堆)。托管堆又分为两种,小对象堆和大对象堆(LOH),小对象堆用来分配常用的资源对象内存(如类,数组等等),小对象堆的内存段进一步划分为3代,0代,1代,2代。(大对象堆用来分配一些大对象和非托管资源,我们后文中专门来解释)
托管堆初始化时,不包含任何对象,当我们声明一个对象时,这个对象称为第0代对象。也就是说,第0代对象就是那些新构造的对象,而且垃圾回收器没有检查过的对象。例如下图中,托管堆中分配了A,B,C,D,E5个对象,它们就是第0代对象。
接下来随着我们不停地分配对象,第0代的堆内存使用完毕,且这随着程序的流转,C和E变得不可达,当我们分配下一个内存F时,CLR就会执行一次垃圾回收。此时,C,E对象内存被回收掉,我的的ABD对象从第0代对象变为第1代对象。这时候,垃圾回收结束,第0代不包含任何对象。如下图所示:
接下来随着程序的运行,又在0代中分配了对象F G H I J K,1代对象中B变得不可达。接下来给对象L分配内存时内存不足,将执行垃圾回收。CLR会为第0代对象和第 1代对象选择预算,由于第一代中的占用内存远少于预算,所以垃圾回收期只检查第0代的对象(基越新的对象获得越短),因为第0代对象包含更多的垃圾可能性更大,可以回收更多的内存。忽略了第一代中的对象,所以加快了垃圾回收速度。
随着垃圾回收的不断进行,第1代的内存将不断增加,当第1代对象的内存增长到占用了占用了全部预算(0代给新对象分配内存就要进行GC),此时,会进行第1代的垃圾回收,幸存下来的对象被分配的第2代中去。托管堆只支持3代(0,1,2)。超过85000字节的对象称之为大对象,直接由第2代分配内存。
代给GC带来的性能提升主要体现在不必遍历托管堆中的每一个对象。如果根或者对象引用了老一代的某个对象,垃圾回收期就可以忽略老对象内部所有引用(CLR的特征,引用跟踪算法,同步索引块中的一位标识),在更短的时间内构造好可达对象图。假如老对象字段引用了新对象,则由JIT编辑器内部的一个机制(单独解释)让垃圾回收期跳过。微软官方性能测试,0代执行一次GC,花费时间不少过1毫秒。
- JIT的机制是在对象引用字段发生变化时候,设置一个对应位标志。这样,下一次GC回收资源内存时候,会知道上一次GC过后,哪些老对象被写入位标志,这样,只有位标志发生变化(也就是老对象字段发生变化)时候,才检查老对象是否引用第0代对象。
3、特殊类型的清理
特殊类型:大多数对象只要分配内存就可以使用。但是,还有部分对象需要分配本机资源(例如文件,网络连接,套接字,互斥体),我们称这部分对象为特殊类型的资源。
特殊类型的回收过程和特点:包含本机资源的类型被GC时,GC在回收内存之前,需要将本机资源终结(Finalization)。当CLR判定一个特殊类型的对象不可达时,对象将终结自己,释放包裹的本机资源,然后由GC回收其内存。
Object基类型定义了虚方法Finalize,GC判定对象时垃圾后,调用对象的Finalize方法,这个方法一般以析构函数的形式出现。(ILSpy 反编译后的析构函数代码为protected override Finalize)。
特殊类型注意事项:
- Finalize执行在GC之后,所以特殊类型的对象不是马上被GC回收,因为Finalize方法可能要访问对象字段。这可能使对象提升到另一级别的代,增加内存耗用。所以,尽量避免引用类型的字段定义为可终结对象。
- Finalize方法执行时无顺序的。所以不要在Finalize方法中访问定义了其他Finalize方法的类型,因为另一个类型对象可能已被终结。
- CLR用一个特殊的、高优先级专用线程调用Finalize方法避免死锁。
- 自定义包含了本机资源的托管类型时要继承自SafeHandle(派生自它保证本机资源在GC时被释放)。
控制包装了本机资源类型对象的生存期:
例如这里我们要往D盘的1.txt中写入一部分文本,然后写完后想把这个文件删除,此时就会报 “System.IO.IOException:“文件“d:\1.txt”正由另一进程使用,因此该进程无法访问此文件。”这样一个异常,这是因为本机资源未被释放(Finalize)。假如我们想控制包装本机资源的类型对象的生命周期,就要实现IDispose接口。(如果类型对象的其中一个字段实现了这个接口,那么这个类型也就实现了Dispose模式。)然后我们修改我们的代码,成功删除文件。
终结的内部实现原理:
包装了本机资源的对象被回收时,会调用Finalize方法。
包装了本机资源的对象创建的时候(定义了Finalize方法),在从堆中分配内存前,会将这个对象的指针添加到一个终结列表(由GC控制的内部数据结构)中。这个列表中的每一项,都指向一个定义了Finalize方法的对象,回收这些对象内存之前应该先调用它的Finalize方法(这里注意,虽然Object也定义了Finalize方法,但是CLR会忽略它,只有重写了Finalize方法的类型对象才会加入到终结列表)。如下图所示,C,E,F,I,J是定义了Finalize方法的类型对象,指向它们的指针被加入到终结列表中:
垃圾回收开始进行,B,E,G,H,I,J被判定为垃圾,这时候垃圾回收器会扫描终结列表来查找这些对象的引用(这里找到了E,I,J),然后把这些引用从终结列表中移除,附加到freachable队列(也是GC的一个内部数据结构)。在freachable队列中的每一个引用都代表即将进行Finalize调用的对象。经历过一轮GC后,堆内存如下所示:
CLR使用一个高优先级的,专用的线程来调用Finalize方法,这个线程避免潜在的线程同步问题。当freachable队列为空时候,这个线程将休眠,freachable队列出现记录项,将唤醒这个线程。这样来看,包装了本机资源的托管对象至少要进行两次GC才能回收它的内存,第一次由专用线程来执行Finalize方法,第二次才由GC回收这个对象的内存(大于2次是因为这些对象可能被提高到老的一代)。
4、手动监控和控制对象生命周期
CLR为每一个AppDomain都提供了一个GC Handle table,允许程序监视或者控制对象的生命周期。这个表中的每一条记录项都包含托管堆中一个对象的引用和监视控制对象标志。这里注意一个类GCHandle和一个枚举对象 GCHandleType。
GCHandle调用Alloc方法时候,会扫描AppDomain的GC Handle table,查找一个可用的记录项存储对象的生命周期并且传回给对象引用。GChandle的Target属性,返回句柄表示的对象,如下图所示:
GC发生时候会使用GC Handle table,首先,GC将所有对象标识为将要回收,扫描GC Handle table,所有GCHandleType为Normal和Pinned对象标识为根;然后查找GCHandleType为Weak的项,如果引用了未标记的对象,那么这个对象就是垃圾,且把这个项赋值为null;GC继续扫描中介列表,将无引用标识对象的引用放入freachable队列;GC再扫描GC Handle table,查找GCHandleType 为WeakTrackResurrection的记录想,这些记录想引用了未标记的对象(freachable队列中)变为垃圾,这些记录项赋值为Null。最后GC对内存进行压缩。
浅析CLR的GC(垃圾回收器)的更多相关文章
- .NET GC垃圾回收器
GC垃圾回收器简介 全名: Garbage Collector 原理: 以应用程序的根(root)为基础,遍历应用程序堆(heap)上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是已经死亡 ...
- [Java基础]-- Java GC 垃圾回收器的分类和优缺点
https://blog.csdn.net/high2011/article/details/80177473?utm_source=blogxgwz2 参考:elasticsearch实战-使用G1 ...
- 一篇文章让你了解GC垃圾回收器
简单了解GC垃圾回收器 了解GC之前我们首先要了解GC是要做什么的?顾名思义回收垃圾,什么是垃圾呢? GC回收的垃圾主要指的是回收堆内存中的垃圾对象. 从根对象出发,所有被引用的对象,都是存活对象 其 ...
- 如何获取GC(垃圾回收器)的STW(暂停)时间?
前言 在现代的容器化和微服务应用中,因为分布式的环境和错综复杂的调用关系,APM(Application Performance Monitoring 应用性能监控)显得尤为重要,它通过采集应用程序各 ...
- GC垃圾回收器
java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”.jvm解决的两个问题:给对象分配内存以及回收分配给对象的内存.GC:将内存中不再被使用的对象进行回收.GC的作用域是JVM运行时 ...
- GC(垃圾回收器)中的算法
GC的两种判定方法 (1) 引用计数法 给对象添加一个引用计数器,每当引用一次+1,每次失效时-1,当计数器为0时,表示对象就是不可能再被使用的. (2) 可达性分析算法 将“GC Roots”对象作 ...
- Java虚拟机笔记(二):GC垃圾回收和对象的引用
为什么要了解GC 我们都知道Java开发者在开发过程中是不需要关心对象的回收的,因为Java虚拟机的原因,它会自动回收那些失效的垃圾对象.那我们为什么还要去了解GC和内存分配呢? 答案很简单:当我们需 ...
- Android内存优化5 了解java GC 垃圾回收机制3
引言 接App优化之内存优化(序), 作为App优化系列中内存优化的一个小部分. 由于内存相关知识比较生涩, 内存优化中使用到的相关工具, 也有很多专有名词. 对Java内存管理, GC, Andro ...
- 牛客网Java刷题知识点之垃圾回收算法过程、哪些内存需要回收、被标记需要清除对象的自我救赎、对象将根据存活的时间被分为:年轻代、年老代(Old Generation)、永久代、垃圾回收器的分类
不多说,直接上干货! 首先,大家要搞清楚,java里的内存是怎么分配的.详细见 牛客网Java刷题知识点之内存的划分(寄存器.本地方法区.方法区.栈内存和堆内存) 哪些内存需要回收 其实,一般是对堆内 ...
随机推荐
- 集合运算(UNION)
表的加法 集合运算:就是满足统一规则的记录进行的加减等四则运算. 通过集合运算可以得到两张表中记录的集合或者公共记录的集合,又或者其中某张表中记录的集合. 集合运算符:用来进行集合的运算符. UNIO ...
- PAT_A1116#Come on! Let's C
Source: PAT A1116 Come on! Let's C (20 分) Description: "Let's C" is a popular and fun prog ...
- jmeter的性能监控框架搭建记录(Influxdb+Grafana+Jmeter)
查看笔记 http://note.youdao.com/noteshare?id=c700365713abb98bd3d10e6f45393af9&sub=6F4E14FF3F9D4167AE ...
- 【剑指Offer】54、字符流中第一个不重复的字符
题目描述: 请实现一个函数用来找出字符流中第一个只出现一次的字符.例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g".当从该字 ...
- [luogu2317 HNOI2005] 星际贸易 (dp)
传送门 Solution 两个dp分开处理, 第一问什么都不考虑直接dp 第二问还有些疑惑,姑且先留坑 Code //By Menteur_Hxy #include <cstdio> #i ...
- Linux启用ftp服务及连接
虚拟机的系统是centos6.3 第一步.启动ftp service vsftpd restart 提示 vsftpd: 未被识别的服务 解决方法是升级vsftpd服务 yum install vsf ...
- H5-video1 iOS苹果和微信中音频和视频实现自动播放的方法
<audio preload="preload" controls id="car_audio" src="http://media.xitao ...
- Codeforces 816C/815A - Karen and Game
传送门:http://codeforces.com/contest/816/problem/C 本题是一个模拟问题. 有一个n×m的矩阵.最初,这个矩阵为零矩阵O.现有以下操作: a.行操作“row ...
- 【ZOJ 4060】Flippy Sequence
[链接] 我是链接,点我呀:) [题意] [题解] 按照两个区间的排列方式 我们可以分成以下几种情况 会发现这两个区间的作用 最多只能把两段连续不同的区间变为相同. 那么写个for处理出连续不相同的一 ...
- 利用echarts做图表统计
以项目中的扇形统计图为例: 首先,第一步: 引入外部echarts.js文件 其次,第二步: HTML代码块 <div class="count-body-con count-tj&q ...