在.NET环境下,所有的对象都是通过CLR进行管理,并且由垃圾收集系统来负责回收。我们可以想象得到的是,CLR应当会以某种形式来管理这些对象,并且这些对象与对象之间具有一定的联系。
有一些工具,例如 .NET Memory Profiler等,可以显示出这些关系,但是,可惜的是,这些工具不是免费的。
实际上,Visual Studio .NET本身已经具有这样的功能,只是没有在文档中介绍,并且比上述的工具要复杂一些而已。我们以下面这个小程序来示例如何使用这些功能:
using System;
using System.Collections;
namespace test
{
class StringHolder
{
public string StringData;
public StringHolder(string stringData)
{
StringData = stringData;
}
}
class TestClass
{
public ArrayList Holders;
[STAThread]
static void Main(string[] args)
{
TestClass testClass = new TestClass();
testClass.Holders = new ArrayList();
for (int i = 0; i < 10; ++i)
{
testClass.Holders.Add(new StringHolder("Hello"));
}
Console.ReadLine();
}
}
}
这个程序的功能还是很简单的,我们在 Console.ReadLine()处设置一个断点,然后按下F5编译并运行程序,Visual Studio .NET会在Console.ReadLine()处暂停。需要注意的是,在编译前需要设定项目属性,在Properties Pages的Configuration Properties条目中的Debugging项中,打开Enable Unmanaged Debugging(设置为True)。
然后我们选择Visual Studio .NET的Debug菜单,并且选择Windows->Immediate,这时候会显示一个Command Window – Immediate 窗口。在这个窗口中输入下面文本:
.load sos
这条命令的作用是装入SOS.DLL,这是一个WinDbg的扩展库,用于调试托管代码。
随后输入:
!dumpheap –stat
该命令要求显示程序中所有对象的统计信息,输入该命令后,在Command Window – Immediate窗口中会显示如下信息:
total 61 objects
Statistics:
MT Count TotalSize Class Name
b550ac 1 12 test.TestClass
79c18514 1 20 System.AppDomainSetup
79c20d74 1 24 System.Collections.ArrayList
79c15614 1 32 System.SharedStatics
79c13fc4 2 40 System.Text.StringBuilder
79c14ee4 1 64 System.ExecutionEngineException
79c14dac 1 64 System.StackOverflowException
79c14c74 1 64 System.OutOfMemoryException
79c16e8c 1 80 System.AppDomain
b723f8 3 92 System.Char[]
b55134 10 120 test.StringHolder
79c125c8 28 1936 System.String
14dce0 5 6164 Free
b7209c 5 6328 System.Object[]
Total 61 objects
这些信息包含了很多有用的信息,第一栏,MT表示MethodTable的地址,实际上代表了一种类型的信息,第二栏Count表示该类型的实例数量,第三栏TotalSize表示该类型所有实例总共占用了多少字节的空间,以及最后一栏,Class Name表示了该类型的名字。
因此,我们可以看到,test.TestClass,在整个程序中只有一个实例,并且这个实例占用了12个字节。在我们的程序中有一个循环,创建了10个StringHolder,因此在test.StringHolder一行中,注明了test.StringHolder有10个实例,总共占用120字节内存。
最后,我们看到有28个String,可是在我们的程序中至多只有20个字符串,那么这些字符串又是从哪里来的呢?
我们输入下面这个命令:
!dumpheap -mt 79c125c8
该命令显示MethodTable 79c125c8的详细信息,结果是:
Address MT Size
0129117c 79c125c8 28
012911f0 79c125c8 204
012912bc 79c125c8 20
01291308 79c125c8 40
01291330 79c125c8 76
0129137c 79c125c8 32
0129139c 79c125c8 84
012913f0 79c125c8 80
01291440 79c125c8 188
012914fc 79c125c8 36
01291534 79c125c8 52
01291568 79c125c8 32
01291588 79c125c8 32
012915a8 79c125c8 20
012915bc 79c125c8 32
012915dc 79c125c8 64
0129161c 79c125c8 48
01291660 79c125c8 36
01291684 79c125c8 84
012916d8 79c125c8 52
0129170c 79c125c8 108
01291778 79c125c8 88
012917d0 79c125c8 48
01291800 79c125c8 112
01291870 79c125c8 60
012918ac 79c125c8 204
01291978 79c125c8 48
012919b8 79c125c8 28
Bad MethodTable for Obj at 01291ac0
Last good object: 01291ab4
total 28 objects
Statistics:
MT Count TotalSize Class Name
79c125c8 28 1936 System.String
Total 28 objects
这个表列出了所有字符串的实例,第一栏是实例的地址,第二栏是实例所属MT的地址,第三栏是实例所占的字节数。我们可以通过下面这条命令来显示一个实例的所属关系:
!gcroot 012919b8
该命令要求调试器查找地址012919b8这个对象实例的信息,输出如下:
Scan Thread 1748 (6d4)
ESP:12f2ec:Root:012919e0(System.Collections.ArrayList)->012919f8(System.Object[])->01291ab4(test.StringHolder)->012919b8(System.String)
ESP:12f318:Root:01291ab4(test.StringHolder)->012919e0(System.Collections.ArrayList)
ESP:12f3b0:Root:01291ab4(test.StringHolder)->012919e0(System.Collections.ArrayList)
ESP:12f458:Root:01291ab4(test.StringHolder)->012919e0(System.Collections.ArrayList)
ESP:12f464:Root:01291ab4(test.StringHolder)->012919e0(System.Collections.ArrayList)
Scan Thread 3364 (d24)
Scan HandleTable 1571f8
Scan HandleTable 14a5b0
从这个结果可以得到我们所希望看到的信息:
Root:012919e0是一个ArrayList,从它连接到012919f8,一个Object数组,然后连接到01291ab4,这是一个StringHolder,最后连接到012919b8,我们所查找的字符串。
然后我们来看一下StringHolder的内容。输入下面命令:
!dumpobj 01291ab4
该命令会显示一个对象的内容:
Name: test.StringHolder
MethodTable 0x00b55134
EEClass 0x00f234ec
Size 12(0xc) bytes
mdToken: 02000002 (C:"Documents and Settings"Andrew"My Documents"Visual Studio Projects"test"bin"Debug"test.exe)
FieldDesc*: 00b550f8
MT Field Offset Type Attr Value Name
00b55134 4000001 4 CLASS instance 012919b8 StringData
上面最后两行构成了一个表格,表示这个对象中所有字段的值。在StringHolder中只有一个字段,是StringData,它的值,指向012919b8,也就是上面看到的连接的字符串。
为了找到ArrayList的所属者,我们继续输入:
!gcroot 012919e0
结果是:
Scan Thread 1748 (6d4)
ESP:12f2ec:Root:012919e0(System.Collections.ArrayList)->012919e0(System.Collections.ArrayList)
ESP:12f310:Root:012919e0(System.Collections.ArrayList)->012919e0(System.Collections.ArrayList)
ESP:12f454:Root:012919e0(System.Collections.ArrayList)->012919e0(System.Collections.ArrayList)
ESP:12f45c:Root:012919d4(test.TestClass)->012919e0(System.Collections.ArrayList)
Scan Thread 3364 (d24)
Scan HandleTable 1571f8
Scan HandleTable 14a5b0
可以看到ArrayList被012919d4,也就是TestClass连接。这个对象就是我们定义在Main()中的那个testClass。