• 透过WinDBG的视角看String


    摘要 : 最近在博客园里面看到有人在讨论 C# String的一些特性. 大部分情况下是从CODING的角度来讨论String. 本人觉得非常好奇, 在运行时态, String是如何与这些特性联系上的. 本文将侧重在通过WinDBG来观察String在进程内的布局, 以此来解释C# String的一些特性.

    问题

    C# String有两个比较有趣的特性.

    1. String的恒定性. 字符串横定性是指一个字符串一经创建,就不可改变。那么也就是说当我们改变string值的时候,便会在托管堆上重新分配一块新的内存空间,而不会影响到原有的内存地址上所存储的值。
    2. String的驻留. CLR runtime通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统中只有一个。

    对应着两个特性, 我产生了一些疑问.

    • String的恒定性是怎么样让string进行比较的时候出现有趣的结果的? 它的比较结果为什么会与其他引用类型的结果不一样?
    • 什么样的String会被放到拘留池中?
    • 拘留池是怎样的数据结构? 它真是个Hashtable吗?
    • 驻留在拘留池内的String会不会被GC,  它的生命周期会有多长(什么时候才会被回收)?

    String的恒定性

    先看一下下面的例子 :

    private static void Comparation()
    {
        string a = "Test String";
        string b = "Test String";
        string c = a;
    
        Console.WriteLine("a vs b : " + object.ReferenceEquals(a, b));
        Console.WriteLine("a vs c : " + object.ReferenceEquals(a, c));
    
        SimpleObject smp1 = new SimpleObject(a);
        SimpleObject smp2 = new SimpleObject(a);
    
        Console.WriteLine("smp1 vs smp2 : " + object.ReferenceEquals(smp1, smp2));
        Console.ReadLine();
    
    }
    
    class SimpleObject
    {
        public string name = string.Empty;
    
        public SimpleObject(string name)
        {
            this.name = name;
        }
    }


     

    image

    从结果上看, 虽然是不同的变量 a, b, c. 由于字符串的内容是相同的, 所以比较的结果也是完全相同的. 对比SimpleObject的实例, smp1和smp2的值虽然也是相同的,但是比较的结果为false.

    下面看一下运行时, 这些objects的的情况.

    在运行时态, 一切皆是地址. 判断两个变量是否是相同的对象, 直观的可以从它地址是否是相同的地址来进行判断.

    用dso命令打印出栈上对应的Objects. 可以看到Test String”虽然出现了3次, 但是他们都对应了一个地址0000000002473f90 . SimpleObject的对象实例出现了2次, 而且地址不一样, 分别是00000000024776700000000002477688 .

    所以, 在使用String的时候, 实质上是重用了相同的String 对象. 在new一个SimpleObject的实例时候, 每一次new都会在新的地址上初始化该对象的结构. 每次都是一个新的对象.

    0:000> !dso
    OS Thread Id: 0x3f0c (0)
    RSP/REG          Object           Name
    ......
    
    000000000043e730 0000000002473f90 System.String
    000000000043e738 0000000002473f90 System.String
    000000000043e740 0000000002473f90 System.String
    000000000043e748 0000000002477670 ConsoleApplication3.SimpleObject
    000000000043e750 0000000002477688 ConsoleApplication3.SimpleObject
    .......
    
    0:000> !do 0000000002473f90 
    Name: System.String
    MethodTable: 00007ffdb0817df0
    EEClass: 00007ffdb041e560
    Size: 48(0x30) bytes
    GC Generation: 0
    (C:windowsassemblyGAC_64mscorlib2.0.0.0__b77a5c561934e089mscorlib.dll)
    String: Test String
    Fields:
                  MT            Field           Offset                 Type VT             Attr            Value Name
    00007ffdb081f060  4000096        8         System.Int32  1 instance               12 m_arrayLength
    00007ffdb081f060  4000097        c         System.Int32  1 instance               11 m_stringLength
    00007ffdb0819838  4000098       10          System.Char  1 instance               54 m_firstChar
    00007ffdb0817df0  4000099       20        System.String  0   shared           static Empty
                                     >> Domain:Value  0000000000581880:0000000002471308 <<
    00007ffdb08196e8  400009a       28        System.Char[]  0   shared           static WhitespaceChars
                                     >> Domain:Value  0000000000581880:0000000002471be0 <<
    

    当字符串内容发生改变的时候, 任何微小的变化都会重新创建出一个新的String对象. 在我们调用这段代码的时候

        Console.WriteLine("a vs b : " + object.ReferenceEquals(a, b));

    CLR runtime实际上做了两件事情. 为字符"a vs b"分配了到了一个新的地址. 将对比结果与刚才的字符拼接到了一起, 分配到了另外一个新的地址. 如果多次拼接字符串, 就会分配到更多的新地址上, 从而可能会快速的占用大量的虚拟内存. 这就是为什么微软建议在这种情况下使用StringBuilder的原因.

    0:000> !dso
    
    Listing objects from: 0000000000435000 to 0000000000440000 from thread: 0 [3f0c]
    
    Address          Method Table    Heap Gen      Size Type
    …..
    0000000002473fc0 00007ffdb0817df0   0  0         44 System.String a vs b : 
    0000000002474138 00007ffdb0817df0   0  0         52 System.String a vs b : True
    
    …..
    

    String的驻留

    CLR runtime通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统中只有一个。 我们看一下如何来理解这句话.

    下面是示例代码 :

    static void Main(string[] args)
    {
        int i = 0;
        while (true)
        {
            SimpleString(i++);
    
            Console.WriteLine( i + " : Run GC.Collect()");
            GC.Collect();
            Console.ReadLine();
        }
    }
    
    private static void SimpleString(int i)
    {
        string s = "SimpleString method ";
        string c = "Concat String";
    
        Console.WriteLine(s + c);
        Console.WriteLine(s + i.ToString());
        Console.ReadLine();
    }

    这是第一次的执行结果. 此时只执行到了SimpleString里面, 还没有从这个方法返回.

    image

    我们可以看到stack上有4个string. 分别是按照代码逻辑拼接起来的string的内容. 从这里我们就可以当我们在拼接字符串的时候, 实际上会在Heap上创建出多个String的对象, 以此来完成这个拼接动作.

    0:000> !dso
    
    Listing objects from: 0000000000386000 to 0000000000390000 from thread: 0 [3f50]
    
    …..
    0000000002a93f70 00007ffdb0817df0   0  0         66 System.String SimpleString method 
    0000000002a93fb8 00007ffdb0817df0   0  0         52 System.String Concat String
    0000000002a93ff0 00007ffdb0817df0   0  0         92 System.String SimpleString method Concat String
    0000000002a97a90 00007ffdb0817df0   0  0         28 System.String 0
    0000000002a97ab0 00007ffdb0817df0   0  0         68 System.String SimpleString method 0
    
    ……

    随意用其中一个来检查它的引用情况.

    从!gcroot的结果看, 这个string被两个地方引用到. 一个是当前的线程. 因为正在被当前线程使用到, 所以能够看到这个非常正常.

    另外一个是root在一个System.Object[]数组上. 这个数组被PINNED在了App Domain 0000000000491880 上面. 这里显示出来, String其实是驻留在一个System.Object[]上面, 而不是很多人猜测的Hashtable. 不过料想CLR 应该有一套机制可以从这个数组中快速的获取正确的String. 不过这点不在本篇的讨论范围之内.

    0:000> !gcroot 0000000002a93f70
    Note: Roots found on stacks may be false positives. Run "!help gcroot" for
    more info.
    Scan Thread 0 OSTHread 81a0
    RSP:b9e9b8:Root:0000000002a93f70(System.String)
    Scan Thread 2 OSTHread 7370
    DOMAIN(0000000000C51880):HANDLE(Pinned):217e8:Root:0000000012a93030(System.Object[])->
    0000000002a93f70(System.String)

    我们可以检查一下这个System.Object[]里面都有什么.

    从这个数组里面可以看到代码中显示声明的的字符串. 第一个元素是一个空值, 这个里面保留的是我们最常用的String.Empty的实例. 第二个元素是”Run GC.Collect()”. 这个在code的里面的main函数中. 当前还没有被执行到, 但是已经被JITed到了该数组中. 其他两个被显示定义的字符串也能够在这个数组中被找到. 另外可以确认的是, 拼接出来的字符串, 临时生成的字符串都没有在这里出现. 然而, 通过拼接出来的String并不在这个数组里面. 虽然拼接出来的String同样分配到了heap上面, 但是不会被收纳到数组中.

    0:000> !dumparray -details 0000000012a93030
    Name: System.Object[]
    MethodTable: 00007ffdb0805be0
    EEClass: 00007ffdb041eb88
    Size: 1056(0x420) bytes
    Array: Rank 1, Number of elements 128, Type CLASS
    Element Methodtable: 00007ffdb08176e0
    [0] 0000000002a91308
        Name: System.String
        MethodTable: 00007ffdb0817df0
        EEClass: 00007ffdb041e560
        Size: 26(0x1a) bytes
         (C:windowsassemblyGAC_64mscorlib2.0.0.0__b77a5c561934e089mscorlib.dll)
        String:         
        Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffdb081f060  4000096        8         System.Int32  1 instance                1 m_arrayLength
        00007ffdb081f060  4000097        c         System.Int32  1 instance                0 m_stringLength
        00007ffdb0819838  4000098       10          System.Char  1 instance                0 m_firstChar
        00007ffdb0817df0  4000099       20        System.String  0   shared           static Empty
                                     >> Domain:Value  0000000000c51880:0000000002a91308 <<
        00007ffdb08196e8  400009a       28        System.Char[]  0   shared           static WhitespaceChars
                                     >> Domain:Value  0000000000c51880:0000000002a91be0 <<
    [1] 0000000002a93f30
        Name: System.String
        MethodTable: 00007ffdb0817df0
        EEClass: 00007ffdb041e560
        Size: 64(0x40) bytes
         (C:windowsassemblyGAC_64mscorlib2.0.0.0__b77a5c561934e089mscorlib.dll)
        String:      : Run GC.Collect()    
        Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffdb081f060  4000096        8         System.Int32  1 instance               20 m_arrayLength
        00007ffdb081f060  4000097        c         System.Int32  1 instance               19 m_stringLength
        00007ffdb0819838  4000098       10          System.Char  1 instance               20 m_firstChar
        00007ffdb0817df0  4000099       20        System.String  0   shared           static Empty
                                     >> Domain:Value  0000000000c51880:0000000002a91308 <<
        00007ffdb08196e8  400009a       28        System.Char[]  0   shared           static WhitespaceChars
                                     >> Domain:Value  0000000000c51880:0000000002a91be0 <<
    [2] 0000000002a93f70
        Name: System.String
        MethodTable: 00007ffdb0817df0
        EEClass: 00007ffdb041e560
        Size: 66(0x42) bytes
         (C:windowsassemblyGAC_64mscorlib2.0.0.0__b77a5c561934e089mscorlib.dll)
        String:     SimpleString method     
        Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffdb081f060  4000096        8         System.Int32  1 instance               21 m_arrayLength
        00007ffdb081f060  4000097        c         System.Int32  1 instance               20 m_stringLength
        00007ffdb0819838  4000098       10          System.Char  1 instance               53 m_firstChar
        00007ffdb0817df0  4000099       20        System.String  0   shared           static Empty
                                     >> Domain:Value  0000000000c51880:0000000002a91308 <<
        00007ffdb08196e8  400009a       28        System.Char[]  0   shared           static WhitespaceChars
                                     >> Domain:Value  0000000000c51880:0000000002a91be0 <<
    [3] 0000000002a93fb8
        Name: System.String
        MethodTable: 00007ffdb0817df0
        EEClass: 00007ffdb041e560
        Size: 52(0x34) bytes
         (C:windowsassemblyGAC_64mscorlib2.0.0.0__b77a5c561934e089mscorlib.dll)
        String:     Concat String    
        Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        00007ffdb081f060  4000096        8         System.Int32  1 instance               14 m_arrayLength
        00007ffdb081f060  4000097        c         System.Int32  1 instance               13 m_stringLength
        00007ffdb0819838  4000098       10          System.Char  1 instance               43 m_firstChar
        00007ffdb0817df0  4000099       20        System.String  0   shared           static Empty
                                     >> Domain:Value  0000000000c51880:0000000002a91308 <<
        00007ffdb08196e8  400009a       28        System.Char[]  0   shared           static WhitespaceChars
                                     >> Domain:Value  0000000000c51880:0000000002a91be0 <<
    

    继续让代码执行下去, 我们需要来几次GC. 验证一下驻留的字符串是否会在不使用之后被GC掉.

    GC完成之后, 按照所设想的, CallStack上面的String都已经被清除掉了.同时因为已经做过了GC动作, GC heap进过了压缩, 没有被PINNED住的对象地址会发生改变. 所以要验证驻留的String是否会被回收, 可以从驻留数组下手. 由于该数组是被PINNED住, 所以即使发生了GC的动作, 它的地址也不会发生改变. 所以可以通过相同的命令把数组里面驻留的String都列出来.

    结果是与我的预期是一致的. 只有被显示定义的String保留在该数组内, 而这些String不会被回收. 通过拼接零时生产的String, 则不会加入到这个数组内, 在GC发生后, 由于没有被引用而被回收掉.

    0:000> !dumparray -details 0000000012a93030
    Name: System.Object[]
    MethodTable: 00007ffdb0805be0
    EEClass: 00007ffdb041eb88
    Size: 1056(0x420) bytes
    Array: Rank 1, Number of elements 128, Type CLASS
    Element Methodtable: 00007ffdb08176e0
    [0] 0000000002a91308
        Name: System.String
        MethodTable: 00007ffdb0817df0
        EEClass: 00007ffdb041e560
        Size: 26(0x1a) bytes
         (C:windowsassemblyGAC_64mscorlib2.0.0.0__b77a5c561934e089mscorlib.dll)
        String:         
    ...
    [1] 0000000002a93f30
        Name: System.String
        MethodTable: 00007ffdb0817df0
        EEClass: 00007ffdb041e560
        Size: 64(0x40) bytes
         (C:windowsassemblyGAC_64mscorlib2.0.0.0__b77a5c561934e089mscorlib.dll)
        String:      : Run GC.Collect()    
    
    …
    [2] 0000000002a93f70
        Name: System.String
        MethodTable: 00007ffdb0817df0
        EEClass: 00007ffdb041e560
        Size: 66(0x42) bytes
         (C:windowsassemblyGAC_64mscorlib2.0.0.0__b77a5c561934e089mscorlib.dll)
        String:     SimpleString method     
        ...
    [3] 0000000002a93fb8
        Name: System.String
        MethodTable: 00007ffdb0817df0
        EEClass: 00007ffdb041e560
        Size: 52(0x34) bytes
         (C:windowsassemblyGAC_64mscorlib2.0.0.0__b77a5c561934e089mscorlib.dll)
        String:     Concat String 

    所以经过上面的观察, 可以得出的结论是驻留的String生命周期非常长. 那么, 在什么时候他才会被回收?

    从上面gcroot的结果, 可以看到主流数组是被PINNED住. 而引用这个数组的App Domain 0000000000C51880.

    用!dumpdomain -stat的命令将所有的app domain信息打印出来. 可以看到这个App Domain是我们代码运行的Domain (ConsoleApplication3.exe). 这个驻留数组是由CLR 来维护, 并且与当前的App Domain联系到一起. 所以, 理论上这些驻留数组的生命周期跟这个App Domain是一致的.

    0:000> !dumpdomain -stat
    --------------------------------------
    System Domain: 00007ffdb1f16f60
    LowFrequencyHeap: 00007ffdb1f16fa8
    HighFrequencyHeap: 00007ffdb1f17038
    StubHeap: 00007ffdb1f170c8
    Stage: OPEN
    Name: None
    --------------------------------------
    Shared Domain: 00007ffdb1f17860
    LowFrequencyHeap: 00007ffdb1f178a8
    HighFrequencyHeap: 00007ffdb1f17938
    StubHeap: 00007ffdb1f179c8
    Stage: OPEN
    Name: None
    Assembly: 000000000047fa60
    --------------------------------------
    Domain 1: 0000000000491880
    LowFrequencyHeap: 00000000004918c8
    HighFrequencyHeap: 0000000000491958
    StubHeap: 00000000004919e8
    Stage: OPEN
    SecurityDescriptor: 0000000000494140
    Name: ConsoleApplication3.exe
    Assembly: 000000000047fa60 [C:windowsassemblyGAC_64mscorlib2.0.0.0__b77a5c561934e089mscorlib.dll]
    ClassLoader: 000000000047f820
    SecurityDescriptor: 000000000047f9a0
      Module Name
    00007ffdb03e1000 C:windowsassemblyGAC_64mscorlib2.0.0.0__b77a5c561934e089mscorlib.dll
    

    写在最后面

    1. String的恒定性. 字符串横定性是指一个字符串一经创建,就不可改变。那么也就是说当我们改变string值的时候,便会在托管堆上重新分配一块新的内存空间,而不会影响到原有的内存地址上所存储的值。
    2. String的驻留. CLR runtime通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统(App Domain)中只有一个。
      直接在CODE里面声明的String会被CLR runtime维护在一个Object[]内.
      临时生成的string或者拼接出来的String不会维护在这个驻留数组中.
      驻留数组的生命周期跟它位于的App Domain一样长. 所以GC并不会影响驻留数组所引用的String, 它们不会被GC.

    可以参考下面这个链接来对这两个特性加深理解.

    http://blog.csdn.net/fengshi_sh/article/details/14837445

    http://www.cnblogs.com/charles2008/archive/2009/04/12/1434115.html

    http://www.cnblogs.com/instance/archive/2011/05/24/2056091.html

  • 相关阅读:
    【转】MyEclipse快捷键大全
    【转】MOCK测试
    【转】万亿移动支付产业的难点和痛点
    【转】【CTO俱乐部走进支付宝】探索支付宝背后的那些技术 部分
    CTO俱乐部
    tomcat修改默认端口
    VS2013试用期结束后如何激活
    项目中遇到的 linq datatable select
    LINQ系列:LINQ to DataSet的DataTable操作
    C#中毫米与像素的换算方法
  • 原文地址:https://www.cnblogs.com/developersupport/p/4212102.html
Copyright © 2020-2023  润新知