使用WinDBG + SOS谈对象大小及字符串的结构
2009-12-01 14:57 by 老赵, 5966 visits昨天我们使用了一个最最简单的小实验,来检查相同类型的不同对象大小是否相同。当然,我们很轻易地“验证”得出,不同长度的字符串大小是不一样的。不过这种表面现象其实很难说明问题,因此我现在还是用WinDBG + SOS来进行一些检查,希望可以得到一些表面上看不出来的信息。
准备工作
首先,您需要先去下载并安装WinDBG(不过,其实它是纯绿色的)。我的机器是32位系统,如果您使用64位的机器进行实验,那么一些结果便会有所不同。不过,大致方式还是差不多的。装好了WinDBG之后,您可以获取任意一个.NET应用程序的Dump文件。例如,您可以随意写一个Console应用程序,就在Main方法里放一个Console.ReadLine调用,然后便可以在程序停止时使用如下命令获取一个Dump文件(其中xxxx为进程的pid):
adplus -p xxxx -hang -quiet
然后您可以打开WinDBG的GUI窗口,配置一个Symbol File Path,例如:
SRV*d:\symbol-cache*http://msdl.microsoft.com/download/symbols
再打开刚才的Dump文件(Open Crash Dump),加载SOS,如:
.load C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll
接着便可以开始了。
检查普通对象大小
首先,随意检查一下堆上有哪些对象:
0:000> !dumpheap -stat total 657 objects Statistics: MT Count TotalSize Class Name 65d7fc7c 1 12 System.__Filters 65d7fc2c 1 12 System.Reflection.Missing 65d73cc4 1 12 System.Security.Permissions.SecurityPermissionFlag 65d6c3a4 1 12 System.Security.Permissions.FileDialogPermission 65d6c368 1 12 System.Security.PolicyManager 65d55604 1 12 System.Security.Permissions.ReflectionPermission ... 65d8224c 3 192 System.IO.UnmanagedMemoryStream 65d80770 18 216 System.Object 65d67864 8 256 System.Collections.ArrayList+SyncArrayList 65d83118 8 288 System.Security.Util.TokenBasedSet 65d81cd4 19 380 System.RuntimeType 65d82818 12 432 System.Security.PermissionSet 65d831a8 9 504 System.Collections.Hashtable 65d82e7c 20 560 System.Collections.ArrayList+ArrayListEnumeratorSimple 65d82b84 30 720 System.Collections.ArrayList 65d832a4 9 1296 System.Collections.Hashtable+bucket[] 65d835c4 10 1852 System.Byte[] 65d82cf0 15 2192 System.Int32[] 65d81784 28 3540 System.Char[] 65d54324 51 18568 System.Object[] 65d80b54 258 21612 System.String
于是我们发现,堆上有许多类型的对象,每个类型的对象数量不一,那么我们检查一下某个特定类型的对象呢?例如PermissionSet对象:
0:000> !dumpheap -mt 65d82818 Address MT Size 01df1ab8 65d82818 36 01df1adc 65d82818 36 01df7288 65d82818 36 01df7370 65d82818 36 01df74e4 65d82818 36 01df7774 65d82818 36 01df7808 65d82818 36 01df7e84 65d82818 36 01dfa4d8 65d82818 36 01dfa514 65d82818 36 01dfa688 65d82818 36 01dfa838 65d82818 36 total 12 objects Statistics: MT Count TotalSize Class Name 65d82818 12 432 System.Security.PermissionSet Total 12 objects
然后看Hashtable:
0:000> !dumpheap -mt 65d831a8 Address MT Size 01df4974 65d831a8 56 01df78c4 65d831a8 56 01df798c 65d831a8 56 01df8924 65d831a8 56 01df8f8c 65d831a8 56 01df9054 65d831a8 56 01df911c 65d831a8 56 01df92ec 65d831a8 56 01dfbd80 65d831a8 56 total 9 objects Statistics: MT Count TotalSize Class Name 65d831a8 9 504 System.Collections.Hashtable Total 9 objects
最后再看看ArrayList:
0:000> !dumpheap -mt 65d82b84 Address MT Size 01df1cec 65d82b84 24 01df2154 65d82b84 24 01df21b8 65d82b84 24 01df2310 65d82b84 24 01df474c 65d82b84 24 01dfa6ac 65d82b84 24 01dfa7f4 65d82b84 24 ... 01dfa85c 65d82b84 24 01dfad48 65d82b84 24 01dfb220 65d82b84 24 01dfb414 65d82b84 24 01dfb574 65d82b84 24 01dfb6d8 65d82b84 24 01dfbb28 65d82b84 24 total 30 objects Statistics: MT Count TotalSize Class Name 65d82b84 30 720 System.Collections.ArrayList Total 30 objects
这样看来,普通类型的每个对象大小都是相同的,不是吗?
字符串的大小及结构
不过,字符串又如何呢?
0:000> !dumpheap -mt 65d80b54 -min 100 Address MT Size 01df11c8 65d80b54 244 01df12bc 65d80b54 292 01df190c 65d80b54 112 01df197c 65d80b54 112 01df19ec 65d80b54 204 01df1b84 65d80b54 296 01df252c 65d80b54 2216 01df2dd4 65d80b54 3384 01df42f4 65d80b54 124 01df4370 65d80b54 188 01df442c 65d80b54 164 01df4524 65d80b54 156 01df45f4 65d80b54 216 01df7a54 65d80b54 120 01df7b1c 65d80b54 296 01df81e4 65d80b54 112 01df85a0 65d80b54 168 01df8664 65d80b54 264 01df9554 65d80b54 148 01df97d4 65d80b54 148 01df9868 65d80b54 276 01df997c 65d80b54 532 01df9b90 65d80b54 1044 01dfa918 65d80b54 280 01dfaba0 65d80b54 280 01dfb284 65d80b54 280 01dfb438 65d80b54 244 01dfb5cc 65d80b54 244 01dfbb4c 65d80b54 244 01dfc050 65d80b54 164 total 30 objects Statistics: MT Count TotalSize Class Name 65d80b54 30 12552 System.String Total 30 objects
由于字符串数量有些多(258个),因此我们只查看那些体积大于100字节的对象。从结果中可以发现,String对象与其它类型不同,它的对象大小不一。那么我们随意查看其中最小的一个会是什么样的:
0:000> !do 01df190c Name: System.String MethodTable: 65d80b54 EEClass: 65b3d65c Size: 110(0x6e) bytes (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) String: C:\Windows\Microsoft.NET\Framework\v2.0.50727\ Fields: MT Field Offset Type VT Attr Value Name 65d82da0 4000096 4 System.Int32 1 instance 47 m_arrayLength 65d82da0 4000097 8 System.Int32 1 instance 46 m_stringLength 65d81834 4000098 c System.Char 1 instance 43 m_firstChar 65d80b54 4000099 10 System.String 0 shared static Empty >> Domain:Value 00336628:01df1198 << 65d81784 400009a 14 System.Char[] 0 shared static WhitespaceChars >> Domain:Value 00336628:01df1898 <<
可以发现,一个String对象包含3个明显的字段,它们的偏移量分别是4、8、12——那么偏移量为0的是什么,一个字符串其他100个字节又存放了什么呢?那么继续,打印出地址上的内容吧:
0:000> dd 01df190c 01df190c 65d80b54 0000002f 0000002e 003a0043 01df191c 0057005c 006e0069 006f0064 00730077 01df192c 004d005c 00630069 006f0072 006f0073 01df193c 00740066 004e002e 00540045 0046005c 01df194c 00610072 0065006d 006f0077 006b0072 01df195c 0076005c 002e0032 002e0030 00300035 01df196c 00320037 005c0037 00000000 00000000 01df197c 65d80b54 0000002f 0000002e 003a0043
上面的数据中,从左往右第一列是地址,而第二列开始则是地址上的数据。从中我们可以看出:
- 偏移量0:String的MethodTable(65d80b54)。
- 偏移量4:m_arrayLength的值47(0000002f)。
- 偏移量8:m_stringLength的值46(0000002e)。
而接下来则是字符串的内容了,此时可以每2字节一看,并注意Byte Order:
- 偏移量12:字符“C”(0043)
- 偏移量14:字符“:”(003a)
- 偏移量16:字符“\”(005c)
- 偏移量18:字符“W”(0057)
- ……
也就是说,String内部似乎就包含了一大堆内存空间,其中包含了字符串的内容。那么,字符串的长度和大小又有什么关系呢?
字符串的“长度”与“容量”
从刚才的分析中,或是.NET Reflector的反编译结果,我们可以知道一个String对象内部包含了三个字段,其中m_stringLength是字符串的长度,那么m_arrayLength又是什么呢?我又随意挑了几个字符串进行查看,不知不觉打开了其中一个1000多字节的字符串:
0:000> !do 01df9b90 Name: System.String MethodTable: 65d80b54 EEClass: 65b3d65c Size: 1042(0x412) bytes (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) String: <PermissionSet class="System.Security.PermissionSet" version="1"> <IPermission class="System.Security.Permissions.SecurityPermission, ..." version="1" Flags="SkipVerification"/> </PermissionSet> Fields: MT Field Offset Type VT Attr Value Name 65d82da0 4000096 4 System.Int32 1 instance 513 m_arrayLength 65d82da0 4000097 8 System.Int32 1 instance 273 m_stringLength 65d81834 4000098 c System.Char 1 instance 3c m_firstChar 65d80b54 4000099 10 System.String 0 shared static Empty >> Domain:Value 00336628:01df1198 << 65d81784 400009a 14 System.Char[] 0 shared static WhitespaceChars >> Domain:Value 00336628:01df1898 <<
这个字符串有些特别,它的长度是273,但是m_arrayLength确是513,因此占用了1042个字节。查看多个字符串我们便可发现,一个字符串所占用的体积其实是16 + m_arrayLength * 2。这显得有些奇怪,不禁让我想到了StringBuilder中“容量”的概念。StringBuilder会维护一个当前最大容量,这样便可以向SB内部不断地添加字符串。但是String对象难道不是不可变的吗?那为什么要保持m_arrayLength这样一个“容量”的概念呢?
这次我们就先探索到这里,我们下次再来看这个问题。
顺风车
有前端开发的兄弟吗?老赵需要你……
我坐个沙发先