代码改变世界

使用WinDBG + SOS谈对象大小及字符串的结构

2009-12-01 14:57 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

昨天我们使用了一个最最简单的小实验,来检查相同类型的不同对象大小是否相同。当然,我们很轻易地“验证”得出,不同长度的字符串大小是不一样的。不过这种表面现象其实很难说明问题,因此我现在还是用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这样一个“容量”的概念呢?

这次我们就先探索到这里,我们下次再来看这个问题。

顺风车

有前端开发的兄弟吗?老赵需要你……