65.9K
CodeProject 正在变化。 阅读更多。
Home

计算托管对象的堆大小

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (7投票s)

2018年7月29日

CPOL

3分钟阅读

viewsIcon

9442

如何在运行时计算堆内存中托管对象的大小

引言

我一直在尝试计算托管对象的堆大小,现在终于搞清楚了。

尝试了这里的方法后,我偶然发现了来自这里的一段代码片段,它看起来很有希望

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中有两种特殊类型具有动态大小; 即,它们的大小因实例而异。 它们是stringsarrays。 因此,运行时使用此公式来计算堆中对象的大小

MT->GetBaseSize() + ((OBJECTTYPEREF->GetSizeField() * MT->GetComponentSize())

换句话说

Base instance size + (length * component size)

例如,object的大小将评估为这样(64位)

24 + (1 * 0) == 24

使用此公式,我们可以计算任何对象的堆大小。

实现

免责声明:这可能被认为是邪恶的。

注意:我将UInt32别名为DWORD,将UInt16别名为WORD

幸运的是,借助StructLayoutFieldOffset属性可以轻松复制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如何计算对象的堆大小的!

© . All rights reserved.