计算托管对象的堆大小






4.88/5 (7投票s)
如何在运行时计算堆内存中托管对象的大小
引言
我一直在尝试计算托管对象的堆大小,现在终于搞清楚了。
尝试了这里的方法后,我偶然发现了来自这里的一段代码片段,它看起来很有希望
Marshal.ReadInt32(typeof(T).TypeHandle.Value, 4)
经过进一步的研究,我发现TypeHandle.Value
实际上是指向类型MethodTable的指针,并且该代码片段从中读取了一个DWORD
。(稍后将详细说明。)
背景
您可能已经知道,托管对象在堆中的布局如下(64位)
偏移量 | 大小 | 类型 |
-8 | 8 | 对象头 |
0 | 8 | MethodTable* |
8 | ... | 字段 |
MethodTable
包含CLR所需的类型信息。 MethodTable
中的前两个字段用于计算堆大小
偏移量 | 大小 | 类型 | 名称 |
0 | 4 | DWORD | m_dwFlags |
4 | 4 | DWORD | m_BaseSize |
如果您还没有注意到,之前的代码片段读取了DWORD
m_BaseSize
。但是,第一个DWORD
在计算大小方面也非常重要。
CLR的工程师在最小化对象大小方面非常有创意。 m_dwFlags
中的最低WORD
是类型的组件大小。 如果该类型是“数组类型”,例如int[]
或string
,则最低WORD
的值将是一个组件的大小(读作:元素)。 例如,对于string
,组件大小将为2(sizeof(char)
),对于int[]
,组件大小将为4(sizeof(int)
)。 另一个WORD
用作标志。
回到上面的代码片段,第二个DWORD
m_BaseSize
是在堆上分配时对象的基本实例大小。 默认情况下,此值为24(64位)或12(32位),因为这是对象的最小大小:
#define MIN_OBJECT_SIZE (2*sizeof(uint8_t*) + sizeof(ObjHeader))
仅m_BaseSize
通常足以计算对象的堆大小,但是在CLR中有两种特殊类型具有动态大小; 即,它们的大小因实例而异。 它们是strings
和arrays
。 因此,运行时使用此公式来计算堆中对象的大小
MT->GetBaseSize() + ((OBJECTTYPEREF->GetSizeField() * MT->GetComponentSize())
换句话说
Base instance size + (length * component size)
例如,object
的大小将评估为这样(64位)
24 + (1 * 0) == 24
使用此公式,我们可以计算任何对象的堆大小。
实现
免责声明:这可能被认为是邪恶的。
注意:我将UInt32
别名为DWORD
,将UInt16
别名为WORD
。
幸运的是,借助StructLayout
和FieldOffset
属性可以轻松复制MethodTable
[StructLayout(LayoutKind.Explicit)]
public unsafe struct MethodTable
{
[FieldOffset(0)] private DWFlags m_dwFlags;
[FieldOffset(4)] private DWORD m_BaseSize;
...
因为只有一个WORD
用于组件大小,所以为了方便起见,我制作了一个单独的struct
来拆分两个WORDS
[StructLayout(LayoutKind.Explicit)]
internal struct DWFlags
{
[FieldOffset(0)] internal WORD m_componentSize;
[FieldOffset(2)] internal WORD m_flags;
...
现在我们有了MethodTable
的表示形式,剩下的就是获取它。 回到TypeHandle.Value
,我们知道它已经指向MethodTable*
,所以现在剩下的就是转换它!
var methodTable = (MethodTable*) typeof(T).TypeHandle.Value;
现在,我们可以在运行时计算任何对象的堆大小。 您可以编写自己的方法来计算它。 这是我的代码示例,以显示如何计算大小
public static int HeapSize<T>(ref T t) where T : class
{
var methodTable = (MethodTable*) typeof(T).TypeHandle.Value;
if (typeof(T).IsArray) {
var arr = t as Array;
return (int) methodTable->BaseSize + arr.Length * methodTable->ComponentSize;
}
if (t is string) {
var str = t as string;
return (int) methodTable->BaseSize + str.Length * methodTable->ComponentSize;
}
return (int) methodTable->BaseSize;
}
注意:我仅对数组类型对象遵循了指定的公式,因为否则该公式仍将评估为基本大小。
现在剩下要做的就是验证它是否有效。
string s = "foo";
HeapSize
给出我们
HeapSize(ref s) == 32
WinDbg
给出我们
!DumpObj /d 000001f98001bc08
Name: System.String
MethodTable: 00007fff1c1a6830
EEClass: 00007fff1ba86cb8
Size: 32(0x20) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\
v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String: foo
这就是GC如何计算对象的堆大小的!