有很多朋友都分析过System.Object作为Dotnet Framework里面的一个基类,她的特性、方法特点及其相关的概念,这篇博文里面,我就从System.Object这个基类的定义以及底层实现的角度,探索这个基类对象在内存里面的布局模型,探索这个基类最本质的面目。
首先,从一个Type的实例在内存里面的布局模型、以及一个实例的各个部分在一个托管进程结构里面对应安排到相应的哪个部分说起。
下面是一个简单的实例结构示意图以及逻辑结构图:
通常所说的对一个Object的引用,实际上这个Object的Ref是指向GC Heap或者是Thread的Heap里面的这个实例的Object Header这部分。而Object Header顾名思义,只是定义部分,它指向的是一个type的MethodTable。每个类型对应着一个MethodTable保存在托管堆里面。这部分的数据,它的存在是为了对一个type进行自描述。
而index指向的是一个叫做SynBlock table的表。每个Object都在这个表里面有一个record。每个record就是我们有的时候可以看到的SyncBlock。保存了相关的一些CLR对这个对象的控制的信息。
我以前在研究这部分的时候,经常看到网上的一些同行们,在说明一个instance的结构的时候指出,一个object的结构,是m_SyncBlockValue后面紧接着MethodTable,然后紧接着Instance Data。需要指出的是,这个理解是将一个Object的逻辑结构和在内存里面的物理结构的一些概念整到了一起,片面而不完整的。
实际上,一个Object的逻辑结构,是由由两个部分组成的。就像上面的第一个图表示的。一个叫做ObjHeader的类,然后就是Object的具体细节实现类。Object类在实现的时候,引用了很多COM+内部对象模型的结构概念。一个ObjHeader,主要包含了两个部分:Bit Flags以及index这两个东西。都包含在m_SyncBlockValue相关的定义里面。
m_SyncBlockValue是一个DWORD类型的数据,4个字节。又叫做:the index and the Bits。里面包含一个LONG的值当作index来用,还有剩下的作为某些特定功能的控制位来使用。在一个实例对象运行的时候,这些位的含义可以自动的被一些opcodes来获取。
m_SyncBlockValue的高六位来标记m_SyncBlockValue的各种不同的功能和用途。m_SyncBlockValue的低26位来保存hash码,或者是SyncBlock的index或者是SpinLock。具体的后26位是标识什么东西,是由前面的6位里面的相关字段来标识出来的。
在lock一个对象时,CLR首先将m_SyncBlockValue当做一个SpinLock使用。如果CLR在有限次的重试后无法获得SpinLock的拥有权,CLR将SpinLock升级为一个AwareLock。AwareLock类似与Windows中的CriticalSection。
这里需要特别说明一下为什么这么设计:
这样设计的好处,在于把不同功能级别的操作或者是控制代码分离开了。数据和控制代码分离。对于这个instance的使用者来说,instance data是最为一个实例结构的最后一部分保存在GC Heap或者是Thread Heap里面的。而对这个instance在运行的时候需要的数据或者是控制代码,放在了MethodTable里面,从ObjHeader可以获得到操作一个instance的时候的这些数据或者代码。而由于程序是运行在托管的环境下面的,为了支持一些CLR的特性,我们需要给这个instance一些CLR或者说是托管环境相关的属性。这些东西,是作为SyncBlock Table里面的一个表项保存在dotnet Framework执行引擎的内存空间里面的。这样,就实现了不同级别的数据的隔离。
我们知道,一个type的实例,也就是对象的分配,是在GC Heap上面进行分配的。一个instance和这个instance相关联的type的相关的不同类型的数据类型,在不同的地方分配,并不是连续在一起分配的。譬如MethodTable,一些static字段,还有相关的Field或者是MethodDesc,这些东西并不是连续分配在一起的。CLR基于效率和性能上面的考虑,将它们放在了不同的地方。
如果看过我前面对应用程序域的介绍,就会知道,对于每个托管进程,都有一个或者几个私有的AppDomian。MethodTable,就是分配在Private Appdomain的高频堆上面(high-frequency heap),而MethodTable里面的相关的项,FieldDescs、methodDescs是分配在低频堆里面的。而native Code,是放在JIT Heap里面的。一个Object的相关数据的分布,并不是连续的,至少有5个或者是以上的Heap来存放这些数据的分布。
另外,SyncBlock也不是在GC Heap的,它更加特殊,是在CLR自己的私有地址空间里面的。
下面,具体详析的探索下这些结构的实现:
首先打开ObjHeader这个类的定义:
************************C++ Code*****************************
class ObjHeader
{
private:
// !!! Notice: m_SyncBlockValue *MUST* be the last field in ObjHeader.
DWORD m_SyncBlockValue; // the Index and the Bits
public:
// 访问Sync Block表里面的相关Syncblock的index。使用位操作来获取。
DWORD GetHeaderSyncBlockIndex()
{
LEAF_CONTRACT;
// pull the value out before checking it to avoid race condition
DWORD value = m_SyncBlockValue;
//对于这一句含义的了解,可以通过对Bits的各个位的含义的获取来了解。
if ((value & (BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX |
BIT_SBLK_IS_HASHCODE)) != BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX)
return 0;
return value & MASK_SYNCBLOCKINDEX;
}
//下面的这个方法演示的是在index里面设置一个bit位的操作方法
void SetIndex(DWORD indx)
{
//注意,这里定义的是一个LONG类型的变量,也就是前面说的m_SyncBlockValue里面包含一个LONG类型的index
LONG newValue;
LONG oldValue;
while (TRUE) {
//获取现有的LONG类型的index
oldValue = *(volatile LONG*)&m_SyncBlockValue;
//在以前的版本里面,这句是没有的,_ASSERTE具体用途没找到,稍后探索
// Access to the Sync Block Index, by masking the Value.
_ASSERTE(GetHeaderSyncBlockIndex() == 0);
// or in the old value except any index that is there -
// note that indx
//could be carrying the BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX bit that we need to preserve
newValue = (indx |
(oldValue & ~(BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX |
BIT_SBLK_IS_HASHCODE | MASK_SYNCBLOCKINDEX)));
//这句主要是为了保证这些位不被随便修改掉。
if (FastInterlockCompareExchange((LONG*)&m_SyncBlockValue,
newValue, oldValue)== oldValue)
{
return;
}
}
}
Object *GetBaseObject()
{
LEAF_CONTRACT;
return (Object *) (this + 1);
}
//省略了若干对index和bit位的操作方法
};
************************C++ Code end************************
在上面的代码中,很多关于bits flag位的定义的不了解,会对这些代码以及内部运行机制带来困难。下面就解释下一个m_SyncBlockValue里面相关的BitFlags的含义:
在m_SyncBlockValue中,使用高位的bit来作为flags标志。在每次获取或者是修改这些位的时候,都需要使用mask来获取这些bits:
// These first three are only used on strings (If the first one is on, we know whether
// the string has high byte characters, and the second bit tells which way it is.
// Note that we are reusing the FINALIZER_RUN bit since strings don't have finalizers,
// so the value of this bit does not matter for strings
#define BIT_SBLK_STRING_HAS_NO_HIGH_CHARS 0x80000000
// Used as workaround for infinite loop case. Will set this bit in the sblk if we have already
// seen this sblk in our agile checking logic. Problem is seen when object 1 has a ref to object 2
// and object 2 has a ref to object 1. The agile checker will infinitely loop on these references.
#define BIT_SBLK_AGILE_IN_PROGRESS 0x80000000
#define BIT_SBLK_STRING_HIGH_CHARS_KNOWN 0x40000000
#define BIT_SBLK_STRING_HAS_SPECIAL_SORT 0xC0000000
#define BIT_SBLK_STRING_HIGH_CHAR_MASK 0xC0000000
#define BIT_SBLK_FINALIZER_RUN 0x40000000
#define BIT_SBLK_GC_RESERVE 0x20000000
// This lock is only taken when we need to modify the index value in m_SyncBlockValue.
// It should not be taken if the object already has a real syncblock index.
#define BIT_SBLK_SPIN_LOCK 0x10000000
#define BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX 0x08000000
// if BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX is clear,
the rest of the header dword is layed out as follows:
// - lower ten bits (bits 0 thru 9) is thread id used for the thin locks
// value is zero if no thread is holding the lock
// - following six bits (bits 10 thru 15) is recursion level used for the thin locks
// value is zero if lock is not taken or only taken once by the same thread
// - following 11 bits (bits 16 thru 26) is app domain index
// value is zero if no app domain index is set for the object
#define SBLK_MASK_LOCK_THREADID 0x000003FF
#define SBLK_MASK_LOCK_RECLEVEL 0x0000FC00
#define SBLK_LOCK_RECLEVEL_INC 0x00000400
#define SBLK_APPDOMAIN_SHIFT 16
#define SBLK_RECLEVEL_SHIFT 10
#define SBLK_MASK_APPDOMAININDEX 0x000007FF
// 如果 BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX 被设置了,并且,如果
BIT_SBLK_IS_HASHCODE 也被设置了, 剩下的dword就是hash Code (bits 0 to 25),
// otherwise the rest of the dword is the sync block index (bits 0 thru 25)
#define BIT_SBLK_IS_HASHCODE 0x04000000
#define MASK_HASHCODE ((1<<HASHCODE_BITS)-1)
#define SYNCBLOCKINDEX_BITS 26
#define MASK_SYNCBLOCKINDEX ((1<<SYNCBLOCKINDEX_BITS)-1)
// Spin for about 1000 cycles before waiting longer.
#define BIT_SBLK_SPIN_COUNT 1000
// The GC is highly dependent on SIZE_OF_OBJHEADER being exactly the sizeof(ObjHeader)
// We define this macro so that the preprocessor can calculate padding structures.
#define SIZEOF_OBJHEADER 4
可以从这些注释里面,可以看到每个字段的大概的用法。
接着,就说到了index里面所指向的东西。也就是index指向的SyncBlock Table里面的记录。首先,m_SyncBlockValue记录的是一个instance的额外的state。如果这个值是0,就说明没有额外的状态。如果它有一个非零的值,CLR就会自动把这个index和
g_pSyncTable里面的SyncTableEntries这个属性一一的对应起来。g_pSyncTable是一个全局的表,也可以说是数组,里面存储每个变量的SyncBlock。
大多数instance的指向SyncBlock的index都是0,和其它的Object一起,共享一个虚构的SyncBlock。SyncBlock的主要作用,是为了object的同步。每个对象的Hash()这个方法的实现,就是根据SyncTableEntry这个字段来实现的。同时,在一个Object(在上面的定义中,我们可以看到,一个Object在最底层的实现,是一个COM+的Object)与底层的COM+交互的过程中,这里也可以存储少量的额外的信息。
这里,插入说一下一个SyncBlock与一个SyncTableEntry的区别。一个Object的m_SyncBlockValue里面的index指向的是g_pSyncTable这个全局表(或者数组)里面的一个记录。这个记录就叫做SyncBlock。而确定index和表里面的哪一个SyncBlock的对应关系的就是每个SyncBlock里面的SyncTableEntry的这个字段。
同时,SyncTableEntry和SyncBlock都是分配非GC回收内存里面的,因为g_pSyncTable就是分配在非GC堆上面,也就是CLR的私有内存里面的。然后,有一个弱类型的pointer指向每个instance的SyncTableEntry这个字段,用于这些对象的回收。
SyncTableEntries在以前版本的framework里面还是以一个结构体的形式出现的,到了后来的版本,就2.0后来的版本,就改写成了一个class:
class SyncTableEntry
{
public:
PTR_SyncBlock m_SyncBlock;
Object *m_Object;
static SyncTableEntry*& GetSyncTableEntry();
};
从上面可以看到,每个SyncTableEntry都包含了一个向前和向后的指针。向后退回到Object,向前指向SyncTableEntry对应的SyncBlock。
在GetSyncTableEntry()方法中,直接返回了g_pSyncTable:
SyncTableEntry*& SyncTableEntry::GetSyncTableEntry()
{
LEAF_CONTRACT;
return g_pSyncTable;
}
对于g_pSyncTable,实际上是一个叫做SyncBlockArray的数组。其定义如下
class SyncBlockArray
{
public:
SyncBlockArray *m_Next;
BYTE m_Blocks[MAXSYNCBLOCK * sizeof (SyncBlock)];
};
在上面的定义的过程中,使用m_Blocks[MAXSYNCBLOCK * sizeof (SyncBlock)]这句,可以保证在大多数只指定了SyncTableEntry而没有指定值的情况下的性能。
OK,截止到这里,对System.Object这个最基本的类,是如何来实现的,及其在内存里面的布局,和针对这个布局程序上是如何实现的,有了一个较为深入的探索。
了解了这些东西,对于探索一个托管引用程序在运行时的特性,以及探索一个托管经常的运行时的内部机制的各个细节问题,都有了一个开山铺路的先驱。
在接下来的2008,将继续深入研究Dotnet最底层的核心运行机制,把这些鲜为人知的“那些事”,都展示出来,抛砖引玉,弥补一下国内大多数人只注重一些编程技巧和控件使用现状的不足吧。
后记:
在后来的修改过程中,更正了几个表达不准确的地方,同时加了两个简单的图,希望有驻与文章的阅读吧。
另外,刚刚百度了一下“g_pSyncTable”,只有一条记录,Google了一下,也刚刚一页,都还是koders上面的。不过找到了一个不错介绍这方面的PPT的PDF:
http://tmd.havit.cz/Teaching/CSharp/Lecture5/ObjectInternals.pdf
希望对对这方面有兴趣的朋友有用。