一.前言
析构函数是一个特殊的函数,它有自己的线程,有自己的实现方式。在CLR里面相当于一个小型的自我运转系统(有的书本把这个称之为终结器)。来看下一些概念以及一些运行模型。
二.概述
析构函数有一堆的概念
1.析构对象列表(也就是存放了包含析构函数的对象),它是最原始的。也就是当进行对象实例化分配的时候,会判断此对象是否包含了析构函数,如果包含了,则把此对象添加到析构对象列表。
flags & GC_ALLOC_FINALIZE
2.析构空闲列表(FreeList==7,也即是不允许被调用的析构函数所在的对象,这些对象存放在这个列表),它是C#里面一个著名的: GC.SuppressFinalize()来启用,以不允许CLR调用虚构函数。前提是首先这个对象里面包含了析构函数,然后才可以设置相应的标志位用以让CLR不执行此对象的析构函数。并且GC运行的时候也会需要这个对象没有存活才可以放入到析构空闲列表。
if (!obj->GetMethodTable ()->HasFinalizer())
return;//这里如果对象不包含析构函数,则直接返回,即使启用了GC.SuppressFinalize也毫无作用。
GCHeapUtilities::GetGCHeap()->SetFinalizationRun(obj);//这里给ObjectHeader设置BIT_SBLK_FINALIZER_RUN标志,当GC进行扫描的时候,发现了这个标志,并且此对象没有被标记存活,那么此对象就放入到析构空闲列表
关于GC运行的时候也会需要判断这个对象没有存活才可以放入到析构空闲列表。
if (!g_theGCHeap->IsPromoted (obj))//GC运想需要判断此对象是否存活,不存活才可以进行下一步
{
//然后会判断是否包含了GC.SuppressFinalize设置的标志,如果包含,则表示此对象的析构函数不运行运行,把这个对象放入到空闲析构队列
if ((obj->GetHeader()->GetBits()) & BIT_SBLK_FINALIZER_RUN)
{
MoveItem (i, Seg, FreeList);//把对象移动到析构空闲队列
obj->GetHeader()->ClrBit (BIT_SBLK_FINALIZER_RUN);//清除GC.SuppressFinalize设置的标记
}
3.关键析构函数列表堆(CriticalFinalizerListSeg==5这个队列目前的情况不是太明了,尚未弄清它是做什么的,先搁置)
4.析构列表堆(FinalizerListSeg==6,它存放的是需要被析构线程调用的析构函数所在的对象)
三.原理
了解了以上概念之后,我们来看下它这些队列的内存模型。首先要明确的一点是这些列表共用一个数组。CLR只是对这个数组进行骚操作,用以区分析构对对象列表,FreeList,CriticalFinalizerListSeg,FinalizerListSeg等四个队列。
图片
图标里面缺少一个析构对象列表,那是因为析构对象列表页是跟它们共用一个地址,也即是m_FillPointers数组值0x100地址。
它们如何操作和分配呢?
1.当对象进行实例化的时候,把包含析构函数的对象添加到析构对象列.
2.当析构函数列表添加完毕之后,在进行GC垃圾回收的时候。在标记对象的动作里面也即是mark_phase里,会对析构对象列表进行扫描。扫描的时候会进行以下动作。
首先,会获取当前GC堆代的起始地址。从这个起始地址开始遍历循环到第三代结尾地址。在这个大循环前提下,里面有个小循环。小循环的作用是找出循环堆里面的析构对象列表。也即是图示的m_FillPointers数组的值。当找到析构对象列表,循环这个析构对象列表里面的对象,判断它是否存活,如果存活则不进行处理。如果不存活,则分情况。分别会移动到
FreeList,CriticalFinalizerListSeg,FinalizerListSeg等三个队列。
FreeList也即是析构空闲列表,它里面包含的对象的析构函数永远不会被调用。FinalizerListSeg里面包含了被调用的析构函数对象。CriticalFinalizerListSeg目的不明确,目前不清楚干什么。
3.当扫描完毕完毕析构对象列表之后,就会启动析构线程。析构线程会调用
FinalizerListSe列表和CriticalFinalizerListSe分别运行里面的析构函数。过程是:这个线程会判断索引6也即是FinalizerListSe和索引5是否相等,如果不相等。则表示有析构函数需要调用,把这个对象取出来,然后调用里面的析构函数。然后会判断索引5也即是
CriticalFinalizerListSe,跟FinalizerListSe同样的方式.
4.RegisterForFinalization(注册析构函数,也即是把有析构函数的对象放到析构列表)
ScanForFinalization(扫描析构列表,也即是区分关键析构列表堆,析构空闲列表等)
GetNextFinalizableObject(调用析构函数)
四.析构线程
析构线程用的是windows事件内核对象来操控的,这里举一个简单的例子
#include <stdio.h>
#include <windows.h>
#include <process.h>
HANDLE g_hEvent;
UINT __stdcall ChildFunc(LPVOID);
int main(int argc, char* argv[])
{
HANDLE hChildThread;
UINT uId;
// 创建一个自动重置的(auto-reset events),未受信的(nonsignaled)事件内核对象
g_hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
hChildThread = (HANDLE)::_beginthreadex(NULL, 0, ChildFunc, NULL, 0, &uId);
// 通知子线程开始工作
printf("Please input a char to tell the Child Thread to work: \n");
getchar();
::SetEvent(g_hEvent);
// 等待子线程完成工作,释放资源
::WaitForSingleObject(hChildThread, INFINITE);
printf("All the work has been finished. \n");
::CloseHandle(hChildThread);
::CloseHandle(g_hEvent);
return 0;
}
UINT __stdcall ChildFunc(LPVOID)
{
::WaitForSingleObject(g_hEvent, INFINITE);
printf(" Child thread is working...... \n");
::Sleep(5*1000); // 暂停5秒,模拟真正的工作
return 0;
}
SetEvent通知线程进行工作,那么析构线程呢,则是CLR在某个时间段通知其进行工作。具体的表现为,注册析构函数,扫描析构函数,这两步完成之后,就会通过SetEvent来通知析构线程,你可以进行工作了。此时析构线程就会从析构对象列表里面取出CriticalFinalizerListSe和FinalizerListSe来调用析构函数。