前言

碰到一些问题,发觉依旧没有全面了解完全析构函数。本篇继续看下析构函数的一些引申知识。

概述

析构函数目前发现的总共有三个标记,这里分别一一介绍下。先上一段代码:

 internal class Program :  IDisposable{
static void Main(string[] args){
StreamReader? streamReader = null;
streamReader = new StreamReader("Test_.dll");
streamReader?.Dispose();
Console.ReadLine();
}
~Program(){
Console.WriteLine("调用了析构函数");
}
public void Dispose(){
this.Dispose();
GC.SuppressFinalize(this);
}
}

这里的析构函数跟Dispose一起混用, ~Program()析构函数会通过Roslyn生成

.method family hidebysig virtual instance void
Finalize() cil managed
{
.override [System.Runtime]System.Object::Finalize
// 代码大小 24 (0x18)
.maxstack 1
IL_0000: nop
.try
{
IL_0001: nop
IL_0002: ldstr bytearray (03 8C 28 75 86 4E 90 67 84 67 FD 51 70 65 ) // ..(u.N.g.g.Qpe
IL_0007: call void [System.Console]System.Console::WriteLine(string)
IL_000c: nop
IL_000d: leave.s IL_0017
} // end .try
finally
{
IL_000f: ldarg.0
IL_0010: call instance void [System.Runtime]System.Object::Finalize()
IL_0015: nop
IL_0016: endfinally
} // end handler
IL_0017: ret
} // end of method Program::Finalize

这里同时需要注意 streamReader?.Dispose();这句话,streamreader实际上继承的是textreader

public class StreamReader : TextReader
{}

所以它调用Dispose的代码是TextReader里面的Dispose:

 public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

也就是关闭了streamReader流。然后base.Dispose.这个base.Dispose实际上就是它的父类TextReader里面的

public void Dispose()
{
this._streamReader.close();
}

Dispose里面的下面一句代码

GC.SuppressFinalize(this);

它是重点。

GC.SuppressFinalize

1.判断当前类是否有析构函数

如果类里面有析构函数,比如例子里的Program,则会设置MethodTable的成员m_dwFlags

m_dwFlags |= enum_flag_HasFinalizer(0x00100000);

它的设置逻辑是如果存在析构函数,并且当前方法不是接口,不是虚方法,方法的索引小于当前类宗的索引数,当前的方法不是Object.Finlize()。那么说明当前这个类有析构函数,所以需要在当前类的MethodTable上进行操作,也即上面的m_dwFlags位设置。

逻辑代码如下:

//存在析构函数,并且当前方法不是接口,不是虚方法
if (g_pObjectFinalizerMD && !IsInterface() && !IsValueClass())
{
WORD slot = g_pObjectFinalizerMD->GetSlot();
//方法的索引小于当前类宗的索引数,当前的方法不是Object.Finlize()
if (slot < bmtVT->cVirtualSlots && (*bmtVT)[slot].Impl().GetMethodDesc() != g_pObjectFinalizerMD)
{
GetHalfBakedMethodTable()->SetHasFinalizer(); //这个地方就是设置m_dwFlags
//此处省略一万行
}
}

2.调用GC.SuppressFinalize

设置当前类的对象头headerobj|BIT_SBLK_FINALIZER_RUN

当我们调用GC.SuppressFinalize的时候,它会进行判断m_dwFlags或上的enum_flag_HasFinalizer位是否为1,如果位0直接返回,如果为1,则设置对象头。它的判断逻辑如下

if (!obj->GetMethodTable ()->HasFinalizer())//HasFinalizer函数判断m_dwFlags的enum_flag_HasFinalizer位
return;
GCHeapUtilities::GetGCHeap()->SetFinalizationRun(obj);//这里设置当前类的对象头headerobj|BIT_SBLK_FINALIZER_RUN
BIT_SBLK_FINALIZER_RUN定义如下:
#define BIT_SBLK_FINALIZER_RUN 0x40000000

3.对象进行分配空间的时候

设置flags |= GC_ALLOC_FINALIZE

一个对象需要进行空间的分配,当进行空间分配的时候,它会判断当前函数是否包含了析构函数。如果包含了,则设置flags标志最后一位位1.然后在对象分配的时候,把它放入到析构队列里面去。

if (pMT->HasFinalizer())//判断当前类是否包含析构函数
flags |= GC_ALLOC_FINALIZE;//如果包含则设置flags最后一位为1
GC_ALLOC_FINALIZE定义如下:
enum GC_ALLOC_FLAGS
{
GC_ALLOC_NO_FLAGS = 0,
GC_ALLOC_FINALIZE = 1,
GC_ALLOC_CONTAINS_REF = 2,
GC_ALLOC_ALIGN8_BIAS = 4,
GC_ALLOC_ALIGN8 = 8,
GC_ALLOC_ZEROING_OPTIONAL = 16,
GC_ALLOC_LARGE_OBJECT_HEAP = 32,
GC_ALLOC_PINNED_OBJECT_HEAP = 64,
GC_ALLOC_USER_OLD_HEAP = GC_ALLOC_LARGE_OBJECT_HEAP | GC_ALLOC_PINNED_OBJECT_HEAP,
};

当进行对象分配的时候,它会判断falgs最后一位是否为1,如果为1,则把对象放入到析构队列,不为1,则不放入。

CHECK_ALLOC_AND_POSSIBLY_REGISTER_FOR_FINALIZATION(newAlloc, size, flags & GC_ALLOC_FINALIZE); //flags & GC_ALLOC_FINALIZE判断falgs最后一位是否为1.

#define CHECK_ALLOC_AND_POSSIBLY_REGISTER_FOR_FINALIZATION(_object, _size, _register) do {
//这里的register就是flags & GC_ALLOC_FINALIZE的值,下面的逻辑如果对象为空直接返回,如果不为空则判断flags & GC_ALLOC_FINALIZE是否等于1,如果为零直接返回,如果为1,则调用REGISTER_FOR_FINALIZATION,把对象放入析构队列
if ((_object) == NULL || ((_register) && !REGISTER_FOR_FINALIZATION(_object, _size)))
{
STRESS_LOG_OOM_STACK(_size);
return NULL;
}

以上是析构函数,GC.SuppressFinalize,Dispose的最底层逻辑。当然这里还有很多技术问题需要解决。后面再看。

标记的作用

GC.SuppressFinalize问题来了,它的这些标记有什么用呢?这是一个非常绕的问题,分析下。首先的enum_flag_HasFinalizer标记表示当前类包含了析构函数,GC_ALLOC_FINALIZE标记表示当前的类对象需要填充到析构队列里面去。而BIT_SBLK_FINALIZER_RUN标记是最为重要的,它如果被标记了则表示从析构队列里面溢出,不需要运行这个当前类的析构函数。

在GC的标记阶段标记对象是否存活完成之后,它需要对对象的析构队列进行扫描。如果析构队列(SegQueue)里的对象被标记存活,且它的对象头有

BIT_SBLK_FINALIZER_RUN标志,则表示此对象的析构队列里的对象可以移出了,也就是不运行此对象的析构函数。

//这里的ScanForFinalization是在GCScanRoot之运行的,还有一个从析构函数里面取出
//对象运行析构函数则是GCHeap::GetNextFinalizableObject
CFinalize::ScanForFinalization (promote_func* pfn, int gen, BOOL mark_only_p,
gc_heap* hp)
{
//判断对象头是否标记了BIT_SBLK_FINALIZER_RUN
if ((obj->GetHeader()->GetBits()) & BIT_SBLK_FINALIZER_RUN)
{
//如果标记了,则把这个对象移除到FreeList,也即是空闲的析构列表,不然存在于析构列表中
MoveItem (i, Seg, FreeList);
//然后清除掉此对象头BIT_SBLK_FINALIZER_RUN标志
obj->GetHeader()->ClrBit (BIT_SBLK_FINALIZER_RUN);
}
}

欢迎关注我的公众号:jianghupt,后台回复:dotnet7。获取一套.Net7 CLR源码教程。顶级技术分享,文章首发。

.Net析构函数再论(CLR源码级的剖析)的更多相关文章

  1. MapReduce的ReduceTask任务的运行源码级分析

    MapReduce的MapTask任务的运行源码级分析 这篇文章好不容易恢复了...谢天谢地...这篇文章讲了MapTask的执行流程.咱们这一节讲解ReduceTask的执行流程.ReduceTas ...

  2. MapReduce的MapTask任务的运行源码级分析

    TaskTracker任务初始化及启动task源码级分析 这篇文章中分析了任务的启动,每个task都会使用一个进程占用一个JVM来执行,org.apache.hadoop.mapred.Child方法 ...

  3. TaskTracker任务初始化及启动task源码级分析

    在监听器初始化Job.JobTracker相应TaskTracker心跳.调度器分配task源码级分析中我们分析的Tasktracker发送心跳的机制,这一节我们分析TaskTracker接受JobT ...

  4. 源码级调试的XNU内核

    i春秋翻译小组-FWorldCodeZ 源码级调试的XNU内核 无论你是在开发内核扩展,进行漏洞研究,还是还有其他需要进入macOS / iOS内核,XNU,有时你需要附加调试器.当你这样做时,使用源 ...

  5. 源码级强力分析hadoop的RPC机制

    分析对象: hadoop版本:hadoop 0.20.203.0 必备技术点: 1. 动态代理(参考 :http://weixiaolu.iteye.com/blog/1477774 )2. Java ...

  6. 关于JAVA中源码级注解的编写及使用

    一.注解简介: 1.1.什么是"注解": ​ 在我们编写代码时,一定看到过这样的代码: class Student { private String name; @Override ...

  7. MySql轻松入门系列——第二站 使用visual studio 对mysql进行源码级调试

    一:背景 1. 讲故事 上一篇说了mysql的架构图,很多同学反馈说不过瘾,毕竟还是听我讲故事,那这篇就来说一说怎么利用visual studio 对 mysql进行源码级调试,毕竟源码面前,不谈隐私 ...

  8. 准备CLR源码阅读环境

    微软发布了CLR 2.0的源码,这个源码是可以直接在freebsd和windows环境下编译及运行的,请在微软shared source cli(http://www.microsoft.com/en ...

  9. 创作gtk源码级vim帮助文档 tags

    创作gtk源码级vim帮助文档 tags 缘由 那只有看到源码了.在linux源码上有个网站 http://lxr.linux.no /+trees, 可以很方面的查出相应版本的代码实现,gtk没有. ...

  10. .NET框架源码解读之准备CLR源码阅读环境

    微软发布了CLR 2.0的源码,这个源码是可以直接在freebsd和windows环境下编译及运行的,请在微软shared source cli(http://www.microsoft.com/en ...

随机推荐

  1. ClickHouse数据表迁移实战之-remote方式

    1 引言 ClickHouse是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS).我们内部很多的报表.数据看板都基于它进行开发.今天为大家带来remote方式的ClickHouse数据表迁 ...

  2. 【.NET源码解读】深入剖析中间件的设计与实现

    .NET本身就是一个基于中间件(middleware)的框架,它通过一系列的中间件组件来处理HTTP请求和响应.在之前的文章<.NET源码解读kestrel服务器及创建HttpContext对象 ...

  3.  Python + unittest + ddt + HTMLTestRunner + log + excel + mysql + 企业微信通知, 接口自动化框架V2.0,支持多业务处理,仅需维护 excel 用例,无需要编写代码

    Python + unittest + ddt + HTMLTestRunner + log + excel + mysql + 企业微信通知 + Jenkins 实现的接口自动化框架. 项目介绍 接 ...

  4. 动态SQL与静态SQL使用场景

    静态SQL 和动态SQL 的区别 静态SQL(或嵌入式SQL) 是应用程序中的 SQL 语句,它们在运行时不会更改,因此可以硬编码到应用程序中. 动态 SQL是在运行时构造的 SQL 语句:例如,应用 ...

  5. linux 脚本:iptables-nat.sh

    #!/bin/bash # 2022.2.28 by dewan # DNAT configuration. iptables -t nat -F PUB_IFACE="enp125s0f0 ...

  6. 2021-7-7 VUE动态样式

    Vue的动态样式实例1 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> & ...

  7. 面试题 01.03. URL化

    面试题 01.03. URL化 简单 URL化.编写一种方法,将字符串中的空格全部替换为%20.假定该字符串尾部有足够的空间存放新增字符,并且知道字符串的"真实"长度.(注:用Ja ...

  8. pandas 格式化日期

    output_data["ShipDate"] = output_data["ShipDate"].dt.strftime("%Y/%m/%d&quo ...

  9. ABC274 题解

    A 题目:给定 \(A,B\) 输出 \({B}\over{A}\) 保留 \(3\) 位小数. 简答题,和A+B problem 一样,除一除,保留一下小数. B 题目:给定一个 \(n\) 行 \ ...

  10. Xshell远程连接虚拟机及连接故障排查

    用Xshell 远程连接虚拟机 如果按前面博客装好虚拟机,会发现刚装好的虚拟机直接连Xshell连不上,宿主机也ping不通虚拟机,这就需要修改VMware的默认网络配置 修改步骤: 1.在VMwar ...