一张图带你了解.NET终结(Finalize)流程
简介
"终结"一般被分为确定性终结(显示清除)与非确定性终结(隐式清除)
- 确定性终结主要
提供给开发人员一个显式清理的方法,比如try-finally,using。 - 非确定性终结主要
提供一个注册的入口,只知道会执行,但不清楚什么时候执行。比如IDisposable,析构函数。
为什么需要终结机制?
首先纠正一个观念,终结机制不等于垃圾回收。它只是代表当某个对象不再需要时,我们顺带要执行一些操作。更加像是附加了一种event事件。
所以网络上有一种说法,IDisposable是为了释放内存。这个观念并不准确。应该形容为一种兜底更为贴切。
如果是一个完全使用托管代码的场景,整个对象图由GC管理,那确实不需要。在托管环境中,终结机制主要用于处理对象所持有的,不被GC和runtime管理的资源。
比如HttpClient,如果没有终结机制,那么当对象被释放时,GC并不知道该对象持有了非托管资源(句柄),导致底层了socket连接永远不会被释放。
如前所述,终结器不一定非得跟非托管资源相关。它的本质是”对象不可到达后的do something“.
比如你想收集对象的创建与删除,可以将记录代码写在构造函数与终结器中
终结机制的源码
源码
namespace Example_12_1_3
{
internal class Program
{
static void Main(string[] args)
{
TestFinalize();
Console.WriteLine("GC is start. ");
GC.Collect();
Console.WriteLine("GC is end. ");
Debugger.Break();
Console.ReadLine();
Console.WriteLine("GC2 is start. ");
GC.Collect();
Console.WriteLine("GC2 is end. ");
Debugger.Break();
Console.ReadLine();
}
static void TestFinalize()
{
var list = new List<Person>(1000);
for (int i = 0; i < 1000; i++)
{
list.Add(new Person());
}
var personNoFinalize = new Person2();
Console.WriteLine("person/personNoFinalize分配完成");
Debugger.Break();
}
}
public class Person
{
~Person()
{
Console.WriteLine("this is finalize");
Thread.Sleep(1000);
}
}
public class Person2
{
}
}
IL
// Methods
.method family hidebysig virtual
instance void Finalize () cil managed
{
.override method instance void [mscorlib]System.Object::Finalize()
// Method begins at RVA 0x2090
// Header size: 12
// Code size: 30 (0x1e)
.maxstack 1
IL_0000: nop
.try
{
// {
IL_0001: nop
// Console.WriteLine("this is finalize");
IL_0002: ldstr "this is finalize"
IL_0007: call void [mscorlib]System.Console::WriteLine(string)
// Console.ReadLine();
IL_000c: nop
IL_000d: call string [mscorlib]System.Console::ReadLine()
IL_0012: pop
// }
IL_0013: leave.s IL_001d
} // end .try
finally
{
// (no C# code)
IL_0015: ldarg.0
IL_0016: call instance void [mscorlib]System.Object::Finalize()
IL_001b: nop
IL_001c: endfinally
} // end handler
IL_001d: ret
} // end of method Person::Finalize
汇编
0199097B nop
0199097C mov ecx,dword ptr ds:[4402430h]
01990982 call System.Console.WriteLine(System.String) (72CB2FA8h)
01990987 nop
01990988 call System.Console.ReadLine() (733BD9C0h)
0199098D mov dword ptr [ebp-40h],eax
01990990 nop
01990991 nop
01990992 mov dword ptr [ebp-20h],offset Example_12_1_3.Person.Finalize()+045h (00h)
01990999 mov dword ptr [ebp-1Ch],0FCh
019909A0 push offset Example_12_1_3.Person.Finalize()+06Ch (019909BCh)
019909A5 jmp Example_12_1_3.Person.Finalize()+057h (019909A7h)
可以看到,C#的析构函数只是一种语法糖。IL重写了System.Object.Finalize方法。在底层的汇编中,直接调用的就是Finalize()
终结的流程

眼见为实
使用windbg看一下底层。
- 创建Person对象,是否自动进入finalize queue?

可以看到,当new obj 时,finalize queue中已经有了Person对象的析构函数
- GC开始后,是否移动到F-Reachable queue?

可以看到代码中创建的1000个Person的析构函数已经进入了F-Reachable queue
sosex !finq/!frq 指令同样可以输出
- 析构对象是否被"复活"?
GC发生前,在TestFinalize方法中创建了两个变量,person=0x02a724c0,personNoFinalize=0x02a724cc。


可以看到所属代都为0,且托管堆中都能找到它们。
GC发生后



可以看到,Person2对象因为被回收而在托管堆中找不到了,Person对象因为还未执行析构函数,所以还存在gcroot 。因此并未被回收,且内存代从0代提升到1代
终结线程是否执行,是否被移出F-Reachable queue


在GC将托管线程从挂起到恢复正常后,且F-Reachable queue 有值时,终结线程将乱序执行。
并将它们移出队列

析构函数的对象是否在第二次GC中释放?
等到第二次GC发生后,由于对象析构函数已经被执行,不再拥有gcroot,所以托管堆最终释放了该对象,

析构函数如果没有及时执行完成,又触发了一次GC。会不会再次升代

答案是肯定的
终结的开销
- 如果一个类型具有终结器,将使用慢速分支执行分配操作
且在分配时还需要额外进入finalize queue而引入的额外开销 - 终结器对象至少要经历2次GC才能够被真正释放
至少两次,可能更多。终结线程不一定能在两次GC之间处理完所有析构函数。此时对象从1代升级到2代,2代对象触发GC的频率更低。导致对象不能及时被释放(析构函数已经执行完毕,但是对象本身等了很久才被释放)。 - 对象升代/降代时,finalize queue也要重复调整
与GC分代一样,也分为3个代和LOH。当一个对象在GC代中移动时,对象地址也需要也需要在finalization queue移动到对应的代中.
由于finalize queue与f-reachable queue 底层由同一个数组管理,且元素之间并没有留空。所以升代/降代时,与GC代不同,GC代可以见缝插针的安置对象,而finalize则是在对应的代末尾插入,并将后面所有对象右移一个位置
眼见为实
点击查看代码
public class BenchmarkTester
{
[Benchmark]
public void ConsumeNonFinalizeClass()
{
for (int i = 0; i < 1000; i++)
{
var obj = new NonFinalizeClass();
obj.Age = i;
}
}
[Benchmark]
public void ConsumeFinalizeClass()
{
for (int i = 0; i < 1000; i++)
{
var obj = new FinalizeClass();
obj.Age = i;
}
}
}

非常明显的差距,无需解释。
总结
一张图带你了解.NET终结(Finalize)流程的更多相关文章
- 8张图带你理解Java整个只是网络(转载)
8张图带你理解Java整个只是网络 一图胜千言,下面图解均来自Program Creek 网站的Java教程,目前它们拥有最多的票选.如果图解没有阐明问题,那么你可以借助它的标题来一窥究竟. 1.字符 ...
- 47 张图带你 MySQL 进阶!!!
我们在 MySQL 入门篇主要介绍了基本的 SQL 命令.数据类型和函数,在局部以上知识后,你就可以进行 MySQL 的开发工作了,但是如果要成为一个合格的开发人员,你还要具备一些更高级的技能,下面我 ...
- 5000字 | 24张图带你彻底理解Java中的21种锁
本篇主要内容如下: 本篇文章已收纳到我的Java在线文档. Github 我的SpringCloud实战项目持续更新中 帮你总结好的锁: 序号 锁名称 应用 1 乐观锁 CAS 2 悲观锁 synch ...
- 炸裂!MySQL 82 张图带你飞
之前两篇文章带你了解了 MySQL 的基础语法和 MySQL 的进阶内容,那么这篇文章我们来了解一下 MySQL 中的高级内容. 其他文章: 138 张图带你 MySQL 入门 47 张图带你 MyS ...
- 35 张图带你 MySQL 调优
这是 MySQL 基础系列的第四篇文章,之前的三篇文章见如下链接 138 张图带你 MySQL 入门 47 张图带你 MySQL 进阶!!! 炸裂!MySQL 82 张图带你飞 一般传统互联网公司很少 ...
- 产品经理-需求分析-用户故事-敏捷开发 详解 一张图帮你了解Scrum敏捷流程
产品经理-需求分析-用户故事-敏捷开发 详解 用户故事是从用户的角度来描述用户渴望得到的功能.一个好的用户故事包括三个要素:1. 角色:谁要使用这个功能.2. 活动:需要完成什么样的功能.3. 商业价 ...
- 终结 finalize()和垃圾回收(garbage collection)
1.为什么要有finalize()方法? 假定你的对象(并非使用new)获得了一块"特殊"的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以他不知道该如何释放该对象 ...
- 10张图带你深入理解Docker容器和镜像
http://dockone.io/article/783 [编者的话]本文用图文并茂的方式介绍了容器.镜像的区别和Docker每个命令后面的技术细节,能够很好的帮助读者深入理解Docker. Doc ...
- 四张图带你了解Tomcat系统架构
一.Tomcat顶层架构 先上一张Tomcat的顶层结构图(图A),如下: Tomcat中最顶层的容器是Server,代表着整个服务器,从上图中可以看出,一个Server可以包含至少一个Service ...
- 一张图带你了解-常见面试之JUC包详解
面试时经常问到JUC包下的类及特性,现在用一张图总结下
随机推荐
- 【Vue2】Axios、Async+Await、解构赋值
Axios入门使用,Async和Await用法,解构赋值语法 <!DOCTYPE html> <html lang="en"> <head> & ...
- 【WSDL】02 四种客户端调用方式
WSDL概念和一些语法内容: https://www.w3school.com.cn/wsdl/index.asp SOAP概念: https://www.runoob.com/soap/soap-t ...
- 【Tutorial C】05 操作符 & 表达式
基本运算符 C使用运算符(operator)来代表算术运算.例如,+运算符可以使它两侧的值加在一起. 如果您觉得术语"运算符"听起来比较奇怪,那么请您记住那些东西总得有个名称. 与 ...
- 学术写作: These authors contributed equally to this work. —— 共同一作
早些年很少看到论文里面有: These authors contributed equally to this work. 不过现在这种方法在论文中出现的还是比较多的,说白了,这种共同一作的声明其实是 ...
- Singleton bean creation not allowed while singletons of this factory are in destruction
1.背景 一直都是正常运行的程序,检查日志发现有一条报错如下: org.springframework.beans.factory.BeanCreationNotAllowedException: E ...
- FAT32和NTFS文件系统的区别
文件系统对于在计算设备上运行至关重要. 因此,选择正确的文件系统变得很重要. FAT32 和 NTFS 是两个具有显着差异的文件系统. 1.磁盘分区容量区别 NTFS可以支持的分区(如果采用动态磁盘则 ...
- 【牛客刷题】HJ10 字符个数统计
题目链接 简单的说这题就是字符串去重以后检查长度.如果用Java的话,可以遍历字符串,然后利用Set来进行去重,最后统计Set的size就可以了.但是如果是Go语言,则稍微麻烦点.基本思路就是用map ...
- Git回退服务器版本及receive.denyDeleteCurrent配置
https://blog.csdn.net/sunalongl/article/details/52013435 如果某次修改了某些内容,并且已经commit到本地仓库,而且已经push到远程仓库了, ...
- Docker 抓取 buildx 缓存
有时候由于配置的失误,导致构建了好久的镜像没能推送到云或者保存到本地.而如果重新构建,则可能又要全部重来.其实这时候我们可以导出 buildx 中的缓存到本地文件,再将本地文件导入为镜像.这样可以节省 ...
- 【工程应用十二】Bayer图像格式中Hamilton-Adams及Zhang Wu 基于LMMSS算法的Demosaic过程优化。
Demosaic,中文直接发翻译为去马赛克, 但是其本质上并不是去除马赛克,这让很多第一次接触这个名词或者概念的人总会想的更多.因此,一般传感器在采集信息时一个位置只采集RGB颜色中的一个通道,这样可 ...