C#托管堆和垃圾回收(GC)

更新时间:2023-07-12 23:09:46 阅读: 评论:0

C#托管堆和垃圾回收(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。
A0B0C0(不可达)D0     
GC后的堆如下图,ABD三个对象提升为第1代对象,此时⽆第0代对象
A1B1D1             
假设程序继续执⾏到某个时刻时,托管堆如下,其中FGHIJ为第0代对象
根据GC假设的前两条可知,它会优先检查第0代对象,那么GC第0代回收后的托管堆如下,FHIJ提升为第1代对象
A 1 |
送别诗大全100首
B 1 | D 1(不可达)| F 1 | H 1 | I 1 | J 1 |     
---|---|---|---|---|---|---|---|---
随着第1代的增加,GC会发现其占⽤了太多内存,所以会同时检查第0代和第1代对象,如某个时刻的托管堆如下,其中K为第0代对象
A 1 |
B 1 | D 1(不可达)| F 1 | H 1(不可达) | I 1 | J 1 | K 0
---|---|---|---|---|---|---|---|---
GC第1代回收后的托管堆如下,其中ABFIJ都为第2代对象,K为第1代对象。
的诗意
还有⼀些额外的规则需要注意:
在进⾏第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="fal"/>
</runtime>
</configuration>
当然,你也可以通过GCSetting 类的GCLatencyMode 属性对垃圾回收进⾏某些控制(在你没有完全了解影响的情况下,强烈建议不要更改):另外,还有⼀个模式叫做NoGCRegion ,⽤于在程序执⾏关键路径时将GC 线程挂起。但是你不能将该值直接赋值给GCLatencyMode 属性,要通过调⽤System.GC.TryStartGCRegion ⽅法才可以,并调⽤System.GC.EndGCRegion ⽅法结束。A 1B 1D 1(不可达)F 0G 0(不可达)H 0I 0J 0
A 2
B 2F 2I 2J 2K 1                模式
说明Batch
关闭并发GC , framework 版本服务器模式默认值Interactive冰箱
打开并发GC ,⼯作站模式与 core 版本服务器模式的默认值LowLatency
在短期的、时间敏感的操作中(如动画绘制)使⽤这个低延迟模式,该模式会尽⼒阻⽌第2代垃圾回收,因为花费时间较多,只有当内存过低时才会回收第2代。SustainedLowLatency 这个低延迟模式不会导致长时间的GC 暂停,该模式会尽⼒阻⽌⾮并发GC 线程对第2代垃圾回收(但是允许后台GC 线程对其
的回收),只有当内存过低时才会阻塞回收第2代,适⽤于需要迅速响应的应⽤程序(如股票等)。
六、注意事项
静态字段引⽤的对象会⼀直存在,直到⽤于加载类型的AppDomain卸载为⽌
由于碎⽚整理的开销相对较⼤,因此GC在划算时才会进⾏碎⽚整理,并⾮每次都会执⾏。
⼤对象始终为第2代,⽽且⽬前版本GC不会压缩⼤对象,因为移动代价过⾼。
第0代和第1代总是位于同⼀个内存段,⽽第2代可能跨越多个内存段。
七、特殊的Finalize(终结器)
包含本机资源的类型被GC时,GC会回收对象在托管堆中使⽤的内存。但这样会造成本机资源的泄漏,为了处理这种情况,CLR提供了称为终结的机制——允许对象在判定为垃圾之后,但在对象内存被回收前执⾏⼀些代码。在C#中的表⽰如下:
class SomeType
{
// 这是⼀个 Finalize ⽅法
~SomeType() { }
}
其⽣成的IL代码为:
可以看到,C#编译器实际是在模块的元数据中⽣成了名为Finalize的protected override⽅法,并且⽅法主体的代码被放置在try块中,并在finally块中调⽤ba.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 _hasDispod = fal;
public void Dispo()
{
Dispo(true);
//阻⽌GC调⽤ Finalize
GC.SuppressFinalize(this);
}
/// <summary>
/// 如果类本⾝包含⾮托管资源,才需要实现 Finalize
/// </summary>
~MyResourceHog()
{
Dispo(fal);
}
protected virtual void Dispo(bool isDisposing)
{
懒组词
if (_hasDispod) return;
//表明由 Dispo 调⽤
if (isDisposing)
{
//释放托管资源
}
//释放⾮托管资源。⽆论 Dispo 还是 Finalize 调⽤,都应该释放⾮托管资源
_hasDispod = true;
}
}
public class DerivedResourceHog : MyResourceHog
{
//基类与继承类应该使⽤各⾃的标识,防⽌⼦类设置为true时⽆法执⾏基类    private bool _hasDispod = fal;
protected override void Dispo(bool isDisposing)
{
if (_hasDispod) return;
if (isDisposing)
{
//释放托管资源
}
//释放⾮托管资源
ba.Dispo(isDisposing);
_hasDispod = true;
}
}

本文发布于:2023-07-12 23:09:46,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/89/1079043.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:对象   回收   托管   类型   资源   模式   垃圾
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图