前言

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

概述

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

 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. #mac安装Homebrew报错问题:curl: (7) Failed to connect to raw.githubusercontent.com port 443: Connection refused

    我们在打开https://brew.sh/index_zh-cn官网的时候都会给你下面这段代码,粘贴复制就可以安装: /bin/bash -c "$(curl -fsSL https://r ...

  2. Electron App 安装包定制 -- Inno Setup 脚本 Pascal Scripting 初探

    在做 Electron 项目时,有个需求是安装包安装时要给客户机上装上某个软件 在查看 Inno Setup 官网后发现是通过 .iss 脚本编写实现自定义安装过程 可在 .iss 内可以添加脚本为安 ...

  3. 一行命令使用 Docker 编译 Latex 文件,简单优雅

    使用 Docker 编译 LaTeX 文章 LaTeX 是一种常用的排版系统,它可以帮助用户创建漂亮.专业的文档.但是,安装和配置 LaTeX 比较麻烦,特别是对于初学者而言. Docker 是一个开 ...

  4. ChatGPT「代码解释器」正式开放,图片转视频仅需30秒!十大令人震惊的魔法揭秘

    经过超过三个月的等待,ChatGPT「代码解释器」终于全面开放.这是一大波神奇魔法的高潮. OpenAI的科学家Karpathy对这个强大的代码解释器测试版赞不绝口.他把它比作你的个人数据分析师,可以 ...

  5. 万字长文 | Hadoop 上云: 存算分离架构设计与迁移实践

    一面数据原有的技术架构是在线下机房中使用 CDH 构建的大数据集群.自公司成立以来,每年都保持着高速增长,业务的增长带来了数据量的剧增. 在过去几年中,我们按照每 1 到 2 年的规划扩容硬件,但往往 ...

  6. 【RabbitMQ】当队列中消息数量超过最大长度的淘汰策略

    [RabbitMQ]当队列中消息数量超过最大长度的淘汰策略 说明 最近在研究RabbitMQ如何实现延时队列时发现消息进入死信队列的情况之一就是当消息数量超过队列设置的最大长度时会被丢入死信队列,看到 ...

  7. Python数据分析易错知识点归纳(四):Matplotlib

    四.matplotlib 基本特性 import matplotlib.pyplot as plt import numpy as np x = np.linspace(-3, 3, 50) y1 = ...

  8. JVM运行时数据区之堆空间

    JVM运行时数据区之堆空间 1.核心概述 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域.堆区在JVM 启动的时候即被创建,其空间大小也就确定了,是JVM管理的最大一块内存空间. & ...

  9. linux 脚本:iptables-nat.sh

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

  10. 好用工具: windows terminal

    直接在微软商店搜索该软件即可,本文介绍无法使用微软商店的情况. 解题思路 当我们无法下载某个软件时,可直接去Github中寻找该项目,知道该软件资源并下载. 下载地址 https://github.c ...