Hello World
Spiga

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

2009-12-01 14:57 by 老赵, 5847 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这样一个“容量”的概念呢?

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

顺风车

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

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

20 条回复

  1. 迷失的code
    *.*.*.*
    链接

    迷失的code 2009-12-01 14:59:00

    我坐个沙发先

  2. 假正经哥哥
    *.*.*.*
    链接

    假正经哥哥 2009-12-01 15:03:00

    沙发又没了

  3. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-01 15:28:00

    Jeffrey Zhao:
    从中我们可以看出:

    偏移量0:String的MethodTable(65d80b54)。
    偏移量4:m_arrayLength的值47(0000002f)。
    偏移量8:m_stringLength的值46(0000002e)。

    ...

    查看多个字符串我们便可发现,一个字符串所占用的体积其实是16 + m_arrayLength * 2。


    呃嗯……是sizeof(DWORD) * 4 + sizeof(wchar_t) * m_arrayLength = 4*4 + 2*m_arrayLength没错。不过老赵指明了偏移量0、4、8的3个DWORD,还差一个……那是位于偏移量-4的SyncBlock索引,附带GC的mark bit作用。

    编辑:刚才忘说了,32位CLR上对象是在DWORD边界对齐的,所以String的m_arrayLength如果是奇数的话String占的空间在上面的公式基础上还要加2。

  4. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-01 15:54:00

    嗯,最后的那个插播广告的形式可以借鉴。。。。

  5. 老赵
    admin
    链接

    老赵 2009-12-01 15:59:00

    @Ivony...
    博客园不解决博客的个人问题,只能靠首页的顺风车鸟……

  6. Jeffrey Chan
    *.*.*.*
    链接

    Jeffrey Chan 2009-12-01 16:16:00

    我只会简单的DIV+CSS.主要是JS功底不行.

  7. 老赵
    admin
    链接

    老赵 2009-12-01 16:18:00

    @Jeffrey Chan
    我不信你会DIV + CSS……给我看个作品?
    真这样的话,JS是小事,只要你会编程,而且其实我也会JS……

  8. Jeffrey Chan
    *.*.*.*
    链接

    Jeffrey Chan 2009-12-01 16:26:00

    @Jeffrey Zhao
    我真的会耶,以前不是给你看我作品的时候,那些都是我布的局.再不信,我晚上发给你看我在大学时候做的.js主要是不经常用,要用的时候去jq网站找找插件完事.要自己纯手写的话,有点头痛.

  9. 老赵
    admin
    链接

    老赵 2009-12-01 16:29:00

    @Jeffrey Chan
    好吧,晚上给我看看……你JS真不行?其实不就是普通编程么。

  10. Jeffrey Chan
    *.*.*.*
    链接

    Jeffrey Chan 2009-12-01 16:33:00

    @Jeffrey Zhao
    我知道你JS历害,我以前看你视频的时候就知道了.看了大胡子的Js脚本,发现我的Js真的很菜,能看懂,要是自己写的话,肯定写不出.

  11. 老赵
    admin
    链接

    老赵 2009-12-01 16:38:00

    @Jeffrey Chan
    大胡子又不是nb在JS编程上,我始终觉得会编程的人JS一定不是问题。
    // 不在这里聊了,这个话题可以msn上说,还有你的作品……

  12. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-01 16:58:00

    好了,一种新的面试形式出现了。。。。

  13. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-01 17:01:00

    @Jeffrey Zhao
    我写JS的经验是写着写着就栽在浏览器兼容性上了……诶。
    说来,大半个月前我在修一个bug,在IE上用swfobject没能正确加载我们的一个SWF,然后修啊修,一时FF好一时IE好,要么都不好,就是弄不到都好……折腾几天终于在IE上能悬悬乎乎的跑了 TvT

  14. 老赵
    admin
    链接

    老赵 2009-12-01 17:03:00

    @RednaxelaFX
    我都觉得RFX老大干这个是糟踏了……

  15. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-12-01 17:22:00

    Jeffrey Zhao:
    @RednaxelaFX
    我都觉得RFX老大干这个是糟踏了……


    没办法 凌驾于语言的几位老大总会覆盖到各种bug

  16. heros
    *.*.*.*
    链接

    heros 2009-12-01 20:45:00

    一举两得。

  17. 横刀天笑
    *.*.*.*
    链接

    横刀天笑 2009-12-02 08:24:00

    倒是觉得做这样的Debug,真没有必要把WinDbg这尊大神给请出来。用Visual Studio+SOS就足够了。

  18. 老赵
    admin
    链接

    老赵 2009-12-02 10:25:00

    @横刀天笑
    是的,可惜我不会……

  19. 横刀天笑
    *.*.*.*
    链接

    横刀天笑 2009-12-02 10:53:00

    @Jeffrey Zhao
    囧,把你上面输入的命令放到VS的“Immediate Window”里,然后用VS的“Memory Window”代替dd命令,还能直接看出值出来

  20. 老赵
    admin
    链接

    老赵 2009-12-02 10:55:00

    @横刀天笑
    这也可以?有机会试试看。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我