一张图带你了解.NET终结(Finalize)流程 ----续
接上文
https://www.cnblogs.com/lmy5215006/p/18456380
评论区精彩,大佬深入讨论了C#的Finalize最佳实践,感觉有必要整理下来,拓展阅读,开拓眼界。
GC类中几个非常重要的API
- GC.ReRegisterForFinalize
顾名思义,再次注册一个已经注册过的可终结对象。其底层实现逻辑与常规的终结注册过程使用同一个方法。 - GC.SuppressFinalize
禁止执行对象的终结器。CLR对它进行了高度优化。前文说到,Finaze Queue的出与入。都需要移除对象并移动后续元素。而该方法仅执行一个非常高效的操作:在object header设置一个bit标记,终结器线程不会调用被bit设置过的Finalize方法。 - GC.WaitForPendingFinalizers
阻塞常规线程,直到终结器线程将F-Reachable queue所有对象处理完毕。 - GC.KeepAlive
延迟对象的生存期,这在控制激进式根回收有大用
确定性终结
C#引入了IDisposable接口实现确定性终结(显式清除)。作为手动调用非托管资源close/reset/release等方法的上位替代,相当于统一了抽象层。这么做有很多好处,比如代码扫描,可以轻松扫描出实现了IDisposable ,但从未调用Dispose方法的代码。再比如降低代码维护成本,只要无脑调用Disposable方法,而不是下钻到具体实现,才知道应该调用什么方法。
using
using作为语法糖,不做过多描述。在IL层中,转换成try-finally形式。在finally最末尾调用Dispose。
放在末尾有一个好处,避免激进式根回收造成的对象被提前回收
Disposable模式(确定性终结与非确定性终结的结合)
由于C#并没有强制要求一定要使用using或者调用Dispose方法,但人是会失误的,保不齐哪天就忘记主动释放导致了内存泄漏。所以有没有一种两全其美的办法呢?
点击查看代码
public class Test : IDisposable
{
//正常使用dispose方法释放
public void Dispose()
{
Release();
//GC.SuppressFinalize(this);
}
//析构函数作为兜底
~Test()
{
Release();
}
//释放非托管资源的方法
private void Release() { }
}
眼见为实
可以看到,未调用前。objech header的bit位为0,调用GC.SuppressFinalize后,bit位为0x40000000。代表禁止运行析构函数
这种使用IDisposable执行显式清除,再使用终结器执行隐式清除。合二为一的方式,被称为Disposable模式。
最佳实践
当然示例代码并不完美,如同评论区大佬所说,有重复执行的可能。
当然,最佳实践也出现在评论区
@花落心语提供的优秀代码
private bool disposedValue;
protected virtual void Dispose(bool disposing)
{
//双检锁,防止多次执行清理代码
if (!disposedValue)
{
if (disposing)
{
// TODO: 释放托管状态(托管对象)
}
// TODO: 释放未托管的资源(未托管的对象)并重写终结器
// TODO: 将大型字段设置为 null
disposedValue = true;
}
}
// TODO: 仅当“Dispose(bool disposing)”拥有用于释放未托管资源的代码时才替代终结器
~Test()
{
// 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
Dispose(disposing: false);
}
void IDisposable.Dispose()
{
// 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
总结
尽管我们已经在实现上实现了“银弹”,但是请别忘记。因为析构函数的存在,依旧避免不了分配时的开销(使用慢速分支分配内存)。
它只是不再进入f-reachable queue 。但依旧会维护在finalize queue 中。因此移除出finalize queue,也是有开销的。
并且有一个大前提:在合适的时间调用GC.SuppressFinalize方法。否则将前功尽弃。
因此养成良好的习惯,时刻不忘记using对象。且借助工具协助你扫描才是真正的“银弹”。
您要是一个纯托管对象,又没特殊要求。你写析构函数图个啥?
眼见为实
点击查看代码
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("start");
Test();
Debugger.Break();
GC.Collect();
//GC.Collect();
Debugger.Break();
Console.WriteLine("end");
Console.ReadLine();
}
private static void Test()
{
var t = new SuppressDemo();
var t2 = new SuppressDemo();
var t3 = new SuppressDemo();
Console.WriteLine("all obj allco. ");
t.Dispose();
}
}
public class SuppressDemo:IDisposable
{
private void Release() { }
public void Dispose()
{
Release();
GC.SuppressFinalize(this);
}
public SuppressDemo()
{
Console.WriteLine("this is constructor. ");
}
~SuppressDemo()
{
Console.WriteLine("this is finalize.");
Release();
Debugger.Break();
}
}
-
创建了3个SuppressDemo对象,不管有没有执行GC.SuppressFinalize,依旧维护在finalize queue中
-
对象执行GC.SuppressFinalize,会发生什么?
当调用GC.SuppressFinalize方法后,CLR底层会调用GCHeap:SetFinalizationRun方法。将对象的object header 高2位标记为0X40。当终结线程开始运作时,因为不会进入F-Reachable Queue队列。所有没有gc root。GC会直接回收,并同步移除出finalize queue
GC前
GC后