托管堆和垃圾回收(GC)
一、基础
首先,为了深入了解垃圾回收(GC),我们要了解一些基础知识:
CLR
:Common Language Runtime,即公共语言运行时,是一个可由多种面向CLR的编程语言使用的“运行时”,包括内存管理、程序集加载、安全性、异常处理和线程同步等核心功能。- 托管进程中的两种内存堆:
托管堆
:CLR维护的用于管理引用类型对象的堆,在进程初始化时,由CLR划出一个地址空间区域作为托管堆。当区域被非垃圾对象填满后,CLR会分配更多的区域,直到整个进程地址空间(受进程的虚拟地址空间限制,32位进程最多分配1.5GB,而64位最多可分配8TB)被填满。本机堆
:由名为VirtualAlloc的Windows API分配的,用于非托管代码所需的内存。
NextObjPtr
:CLR维护的一个指针,指向下一个对象在堆中的分配位置。初始为地址空间区域的基地址。- CLR将对象分为大对象和小对象,两者分配的地址空间区域不同。我们下方的讲解更关注小对象。
大对象
:大于等于85000字节的对象。“85000”并非常数,未来可能会更改。小对象
:小于85000字节 的对象。
然后明确几个前提:
- CLR要求所有引用类型对象都从托管堆分配。
- C#是运行于CLR之上的。
C#new
一个新对象时,CLR会执行以下操作:
- 计算类型的字段(包括从基类继承的字段)所需的字节数。
- 加上对象开销所需的字节数。每个对象都有两个开销字段:类型对象指针和同步块索引,32位程序为8字节,64位程序为16字节。
- CLR检查托管堆是否有足够的可用空间,如果有,则将对象放入
NextObjPtr
指向的地址,并将对象分配的字节清零。接着调用构造器,对象引用返回之前,NextObjPtr
加上对象真正占用的字节数得到下一个对象的分配位置。
弄清楚以上知识点后,我们继续来了解CLR是如何进行“垃圾回收”的。
二、垃圾回收的流程
我们先来看垃圾回收的算法与主要流程:
算法:引用跟踪算法
。因为只有引用类型的变量才能引用堆上的对象,所以该算法只关心引用类型的变量,我们将所有引用类型的变量称为根
。
主要流程:
1.首先,CLR暂停进程中的所有线程。防止线程在CLR检查期间访问对象并更改其状态。
2.然后,CLR进入GC的标记阶段。
a. CLR遍历堆中的对象(实际上是某些代的对象,这里可以先认为是所有对象),将同步块索引字段中的一位设为0,表示对象是不可达
的,要被删除。
b. CLR遍历所有根
,将所引用对象的同步块索引位设为1,表示对象是可达
的,要保留。
3.接着,CLR进入GC的碎片整理阶段。
a. 将可达对象压缩到连续的内存空间(大对象堆的对象不会被压缩)
b. 重新计算根
所引用对象的地址。
4.最后,NextObjPtr
指针指向最后一个可达对象之后的位置,恢复应用程序的所有线程。
三、垃圾回收的具体细节
CLR的GC是基于代的垃圾回收器
,它假设:
- 对象越新,生存期越短
- 对象越老,生存期越长
- 回收堆的一部分,速度快于回收整个堆
托管堆最多支持三代对象:
- 第0代对象:新构造的未被GC检查过的对象
- 第1代对象:被GC检查过1次且保留下来的对象
- 第2代对象:被GC检查大于等于2次且保留下来的对象
第0代回收只会回收第0代对象,第1代回收则会回收第0代和第1代对象,而第2代回收表示完全回收,会回收所有对象。
CLR初始化时,会为第0代和第1代对象选择一个预算容量(单位:KB)。如下图,CLR为ABCD四个第0代对象分配了空间,如果创建一个新的对象导致第0代容量超过预算时,CLR会进行GC。
A0 | B0 | C0(不可达) | D0 |
---|
GC后的堆如下图,ABD三个对象提升为第1代对象,此时无第0代对象
A1 | B1 | D1 |
---|
假设程序继续执行到某个时刻时,托管堆如下,其中FGHIJ为第0代对象
A1 | B1 | D1(不可达) | F0 | G0(不可达) | H0 | I0 | J0 |
---|
根据GC假设的前两条可知,它会优先检查第0代对象,那么GC第0代回收后的托管堆如下,FHIJ提升为第1代对象
A1 | B1 | D1(不可达) | F1 | H1 | I1 | J1 |
---|
随着第1代的增加,GC会发现其占用了太多内存,所以会同时检查第0代和第1代对象,如某个时刻的托管堆如下,其中K为第0代对象
A1 | B1 | D1(不可达) | F1 | H1(不可达) | I1 | J1 | K0 |
---|
GC第1代回收后的托管堆如下,其中ABFIJ都为第2代对象,K为第1代对象。
A2 | B2 | F2 | I2 | J2 | K1 |
---|
还有一些额外的规则需要注意:
- 在进行第1代回收之前,一般都已经对第0代对象回收了好几次了。
- 如果对象提升到了第2代,它会长期保持存活,基本上只有当GC进行完全垃圾回收(包括0、1、2代的对象)时才会进行回收。
- 如果GC回收第0代时发现回收了大量内存,则会缩减第0代的预算,这意味着GC更频繁,但做的事情也减少了;反之,如果发现没有多少内存被回收,就会增大第0代的预算,这意味着GC次数更少,但每次回收的内存相对要多。对于第1代和第2代对象来说,也是如此。
- 如果回收后发现仍然没有得到足够的内存且无法增大预算,GC就会执行一次完全垃圾回收,如果还不够,就会抛出
OutOfMemoryException
异常。
四、何时进行垃圾回收
- 应用程序
new
一个对象时,CLR发现没有足够的第0代对象预算来分配该对象时 - 代码显式调用
System.GC.Collect()
方法时。注意不要滥用该方法 - Windows报告低内存情况时
- CLR正在卸载AppDomain时。会回收该AppDomain的所有代对象
- CLR正在关闭时。CLR在进程正常终止(而不是通过任务管理器等外部终止)时关闭,会回收进程中的所有对象。
五、垃圾回收模式
CLR启动时,会选择一个GC主模式,该模式不会更改,直到进程终止。
- 工作站:默认的,针对客户端应用程序进行优化。GC造成的时延很低,不会导致UI线程出现明显的假死状态
- 服务器:针对服务器端应用程序进行优化,主要是优化吞吐量和资源利用。
可以在配置文件中告诉CLR使用服务器回收模式:
<configuration>
<runtime>
<gcServer enabled="true"/>
</runtime>
</configuration>
另外,GC还支持两种子模式:并发(默认)和非并发。主要区别在于并发模式中GC有一个额外的后台线程,它能在应用程序运行时并发标记对象。可以在配置文件中告诉CLR不要使用并发回收模式:
<configuration>
<runtime>
<gcConcurrent enabled="false"/>
</runtime>
</configuration>
当然,你也可以通过GCSetting
类的GCLatencyMode
属性对垃圾回收进行某些控制(在你没有完全了解影响的情况下,强烈建议不要更改):
模式 | 说明 |
---|---|
Batch | 关闭并发GC,.net framework 版本服务器模式默认值 |
Interactive | 打开并发GC,工作站模式与 .net core 版本服务器模式的默认值 |
LowLatency | 在短期的、时间敏感的操作中(如动画绘制)使用这个低延迟模式,该模式会尽力阻止第2代垃圾回收,因为花费时间较多,只有当内存过低时才会回收第2代。 |
SustainedLowLatency | 这个低延迟模式不会导致长时间的GC暂停,该模式会尽力阻止非并发GC线程对第2代垃圾回收(但是允许后台GC线程对其的回收),只有当内存过低时才会阻塞回收第2代,适用于需要迅速响应的应用程序(如股票等)。 |
另外,还有一个模式叫做
NoGCRegion
,用于在程序执行关键路径时将GC线程挂起。但是你不能将该值直接赋值给GCLatencyMode
属性,要通过调用System.GC.TryStartGCRegion
方法才可以,并调用System.GC.EndGCRegion
方法结束。
六、注意事项
- 静态字段引用的对象会一直存在,直到用于加载类型的
AppDomain
卸载为止 - 由于碎片整理的开销相对较大,因此GC在划算时才会进行碎片整理,并非每次都会执行。
- 大对象始终为第2代,而且目前版本GC不会压缩大对象,因为移动代价过高。
- 第0代和第1代总是位于同一个内存段,而第2代可能跨越多个内存段。
七、特殊的Finalize(终结器)
包含本机资源的类型被GC时,GC会回收对象在托管堆中使用的内存。但这样会造成本机资源的泄漏,为了处理这种情况,CLR提供了称为终结的机制——允许对象在判定为垃圾之后,但在对象内存被回收前执行一些代码。在C#中的表示如下:
class SomeType
{
// 这是一个 Finalize 方法
~SomeType() { }
}
其生成的IL代码为:
可以看到,C#编译器实际是在模块的元数据中生成了名为Finalize
的protected override
方法,并且方法主体的代码被放置在try
块中,并在finally
块中调用base.Finalize
(本例调用了Object
的终结器)。
那么,终结的内部是如何工作的呢?
new
新对象时,如果该对象的类型定义了Finalize
方法,那么在该类型的实例构造器被调用之前,会将指向该对象的指针放到一个终结列表
中,该列表由GC内部控制。- 当可终结对象被回收时,会将引用从终结列表移动到freachable队列中,该队列由GC内部控制。
- CLR会启用一个特殊的高优先级线程来专门调用
Finalze
方法。freachable队列为空时,该线程将睡眠;但一旦队列中有记录项出现,线程就会被唤醒,将每一项都从freachable队列中移除,并调用每个对象的Finalize
方法。
如果类型的
Finalize
方法是从System.Object
继承的,CLR就不认为该对象是“可终结”的,只有当类型重写了Object
的Finalize
方法时,才会将类型及其派生类型的对象视为“可终结”的。
注意,除非有必要,否则应尽量避免定义终结器。原因如下:
- 可终结对象在回收时,必须保证存活,这就可能导致其被提升为另一代,生存期延长,导致内存无法及时回收。另外,其内部引用的所有对象也必须保证都存活,一些被认为是垃圾的对象在可终结对象回收后也无法直接回收,直到下一次(甚至多次)GC时才会被回收。
- Finalize 方法在GC完成后才会执行,而GC的执行时机无法控制,也就导致该方法的执行时间也无法控制。
- Finalize 方法中不要访问其他可终结对象,因为CLR无法保证多个 Finalize 方法的执行顺序。如果访问了已终结的对象,Finalize 方法抛出未处理的异常,导致进程终止,无法捕捉异常。
在实际项目开发中,想要避免释放本机资源基本不可能,但是我们可以通过规范代码来规避异常,这就需要用到IDisposable
接口了。示例代码如下:
public class MyResourceHog : IDisposable
{
//标识资源是否已被释放
private bool _hasDisposed = false;
public void Dispose()
{
Dispose(true);
//阻止GC调用 Finalize
GC.SuppressFinalize(this);
}
/// <summary>
/// 如果类本身包含非托管资源,才需要实现 Finalize
/// </summary>
~MyResourceHog()
{
Dispose(false);
}
protected virtual void Dispose(bool isDisposing)
{
if (_hasDisposed) return;
//表明由 Dispose 调用
if (isDisposing)
{
//释放托管资源
}
//释放非托管资源。无论 Dispose 还是 Finalize 调用,都应该释放非托管资源
_hasDisposed = true;
}
}
public class DerivedResourceHog : MyResourceHog
{
//基类与继承类应该使用各自的标识,防止子类设置为true时无法执行基类
private bool _hasDisposed = false;
protected override void Dispose(bool isDisposing)
{
if (_hasDisposed) return;
if (isDisposing)
{
//释放托管资源
}
//释放非托管资源
base.Dispose(isDisposing);
_hasDisposed = true;
}
}
托管堆和垃圾回收(GC)的更多相关文章
- C# 托管堆和垃圾回收器GC
这里我们讨论的两个东西:托管堆和垃圾回收器,前者是负责创建对象并控制这些对象的生存周期,后者负责回收这些对象. 一.托管堆分配资源 CLR要求所有的对象都从托管堆分配.进程初始化时,CLR划出一个地址 ...
- cir from c# 托管堆和垃圾回收
1,托管堆基础 调用IL的newobj 为资源分配内存 初始化内存,设置其初始状态并使资源可用.类型的实列构造器负责设置初始化状态 访问类型的成员来使用资源 摧毁状态进行清理 释放内存//垃圾回收期负 ...
- 【C#进阶系列】21 托管堆和垃圾回收
托管堆基础 一般创建一个对象就是通过调用IL指令newobj分配内存,然后初始化内存,也就是实例构造器时做这个事. 然后在使用完对象后,摧毁资源的状态以进行清理,然后由垃圾回收器来释放内存. 托管堆除 ...
- .NET 托管堆和垃圾回收
托管堆基础 简述:每个程序都要使用这样或那样的资源,包括文件.内存缓冲区.屏幕空间.网络连接.....事实上,在面向对象的环境中,每个类型都代表可供程序使用的一种资源.要使用这些资源,必须为代表 ...
- 【CLR】解析CLR的托管堆和垃圾回收
目录结构: contents structure [+] 为什么使用托管堆 从托管堆中分配资源 托管堆中的垃圾回收 垃圾回收算法 代 垃圾回收模式 垃圾回收触发条件 强制垃圾回收 监视内存 对包装了本 ...
- 重温CLR(十五) 托管堆和垃圾回收
本章要讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存.简单地说,本章要解释clr中的垃圾回收期是如何工作的,还要解释相关的性能问题.另外,本章讨论了如何设计 ...
- CLR via C# 读书笔记-21.托管堆和垃圾回收
前言 近段时间工作需要用到了这块知识,遂加急补了一下基础,CLR中这一章节反复看了好多遍,得知一二,便记录下来,给自己做一个学习记录,也希望不对地方能够得到补充指点. 1,.托管代码和非托管代码的区别 ...
- C#托管堆和垃圾回收
垃圾回收 值类型 每次使用都有对应新的线程栈 用完自动释放 引用类型 全局公用一个堆 因此需要垃圾回收 操作系统 内存是链式分配 CLR 内存连续分配(数组) 要求所有对象从 托管堆分配 GC 触发条 ...
- 如何管好.net的内存(托管堆和垃圾回收)
一:C#标准Dispose模式的实现 需要明确一下C#程序(或者说.NET)中的资源.简单的说来,C#中的每一个类型都代表一种资源,而资源又分为两类: 托管资源:由CLR管理分配和释放的资源,即由CL ...
随机推荐
- C#/WPF 计算字串的真实长度,调整控件的宽度
下面函数是经常用到的计算字串长度的方法: private double MeasureTextWidth(String str, string fontName, double fon ...
- Expression Blend学习动画基础
原文:Expression Blend学习动画基础 什么是动画(Animation)? 动画就是时间+换面的组合,画面跟着时间变化.最常见的是flash的动画,还有GIF动态图片. 动画的主要元素 时 ...
- Android系统adb命令查看CPU与内存使用率
1. 打开终端,进入上述目录,如下图所示: 2. 输入adb shell,打开adb命令行,如 ...
- 将多个文本文件内的数据导入到Datagridview
private BindingList listXSxxInfoList = new BindingList(); openFileDialog1.Multiselect = true;//允许选择多 ...
- Qt 5.6 5.8 vs2015 编译静态库版本(有全部的截图)good
安装Qt 去Qt官网下载Qt安装包 安装Qt和源码,一定要勾选source选项 添加bin到系统变量 工具 需要python3和 perl. vs2015 第三方工具,到官方下载安装 在命令行 ...
- Lucene Index Search
转发自: https://my.oschina.net/u/3777556/blog/1647031 什么是Lucene?? Lucene 是 apache 软件基金会发布的一个开放源代码的全文检索 ...
- 如何设计出和 ASP.NET Core 中 Middleware 一样的 API 方法?
由于笔者时间有限,无法写更多的说明文本,且主要是自己用来记录学习点滴,请谅解,下面直接贴代码了(代码中有一些说明): 01-不好的设计 代码: using System; namespace Desi ...
- “真正的工作不是说的天花乱坠”,Torvalds 说, “而是在于细节”(Torvalds 认为成功的项目都是99%的汗水和1%的创新)
在刚刚结束的加利福尼亚州的开源领袖峰会(2月14日-16日)上,Linus Torvalds 接受了外媒的采访,分享了他如何管理 Linux kernel 的开发以及他对工作的态度. “真正的工作不是 ...
- How to Use the Dynamic Link Library in C++ Linux (C++调用Delphi写的.so文件)
The Dynamic Link Library (DLL) is stored separately from the target application and shared among dif ...
- 图形界面编程成就了C++
听有人说C#.VB比C++好是因为做界面方便还算傻得可爱,听有人说用C++做数值计算而不屑于做界面可就对不起咱C++的恩人了.这我可要说道说道. 想当年C++刚出江湖,名门出身,自立门派,想抢Obje ...