.Net析构函数再论(源码剖析)

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

1.前言

本篇继续看下析构函数的一些引申知识。

2.概述

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

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);
                    }
}
再论CLR析构函数.net 6 析构函数的几个特性CLR析构列表是如何添加析构函数类的
责任编辑:武晓燕 来源: 江湖评谈
相关推荐

2023-10-11 14:18:40

C#.Net析构

2021-12-11 19:02:03

函数C++对象

2009-08-14 17:24:28

C#构造函数和析构函数

2009-09-03 13:14:55

C#构造函数C#析构函数

2010-07-20 09:52:27

Perl构造函数

2010-02-04 16:39:26

C++析构函数

2010-01-18 15:53:27

C++析构函数

2010-07-16 17:12:58

Perl析构函数

2009-12-04 17:16:41

PHP析构函数

2011-07-15 01:29:39

C++析构函数

2009-07-30 15:24:13

C#析构函数C#构造函数

2010-02-05 13:35:19

C++虚析构函数

2009-11-24 16:18:14

PHP5析构函数

2009-10-15 17:50:48

VB.NET调用API

2009-09-02 10:49:46

C#调用析构方法

2010-03-29 09:11:02

Python引用计数

2015-12-15 09:58:19

golang环境配置建议

2010-09-07 13:40:02

DIV标签

2009-10-20 08:39:57

VB.NET输出函数

2022-09-27 18:56:28

ArrayList数组源代码
点赞
收藏

51CTO技术栈公众号