摘要 : 最近在博客园里面看到有人在讨论 C# String的一些特性. 大部分情况下是从CODING的角度来讨论String. 本人觉得非常好奇, 在运行时态, String是如何与这些特性联系上的. 本文将侧重在通过WinDBG来观察String在进程内的布局, 以此来解释C# String的一些特性.
问题
C# String有两个比较有趣的特性.
- String的恒定性. 字符串横定性是指一个字符串一经创建,就不可改变。那么也就是说当我们改变string值的时候,便会在托管堆上重新分配一块新的内存空间,而不会影响到原有的内存地址上所存储的值。
- 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;
- }
- }
复制代码
从结果上看, 虽然是不同的变量 a, b, c. 由于字符串的内容是相同的, 所以比较的结果也是完全相同的. 对比SimpleObject的实例, smp1和smp2的值虽然也是相同的,但是比较的结果为false.
下面看一下运行时, 这些objects的的情况.
在运行时态, 一切皆是地址. 判断两个变量是否是相同的对象, 直观的可以从它地址是否是相同的地址来进行判断.
用dso命令打印出栈上对应的Objects. 可以看到Test String”虽然出现了3次, 但是他们都对应了一个地址0000000002473f90 . SimpleObject的对象实例出现了2次, 而且地址不一样, 分别是0000000002477670 和 0000000002477688 .
所以, 在使用String的时候, 实质上是重用了相同的String 对象. 在new一个SimpleObject的实例时候, 每一次new都会在新的地址上初始化该对象的结构. 每次都是一个新的对象.
- 0:000> !dso
- OS Thread Id: 0x3f0c (0)
- RSP/REG Object Name
- ......
- 000000000043e730 <strong>0000000002473f90</strong> System.String
- 000000000043e738 <strong>0000000002473f90</strong> System.String
- 000000000043e740 <strong>0000000002473f90</strong> 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:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
- String: <strong>Test String</strong>
- 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的驻留
CLR runtime通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统中只有一个。 我们看一下如何来理解这句话.
下面是示例代码 :
- 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 <strong>a vs b :</strong>
- 0000000002474138 00007ffdb0817df0 0 0 52 System.String <strong>a vs b : True</strong>
- …..
复制代码
这是第一次的执行结果. 此时只执行到了SimpleString里面, 还没有从这个方法返回.
我们可以看到stack上有4个string. 分别是按照代码逻辑拼接起来的string的内容. 从这里我们就可以当我们在拼接字符串的时候, 实际上会在Heap上创建出多个String的对象, 以此来完成这个拼接动作.- 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();
- }
复制代码
随意用其中一个来检查它的引用情况.
从!gcroot的结果看, 这个string被两个地方引用到. 一个是当前的线程. 因为正在被当前线程使用到, 所以能够看到这个非常正常.
另外一个是root在一个System.Object[]数组上. 这个数组被PINNED在了App Domain 0000000000491880 上面. 这里显示出来, String其实是驻留在一个System.Object[]上面, 而不是很多人猜测的Hashtable. 不过料想CLR 应该有一套机制可以从这个数组中快速的获取正确的String. 不过这点不在本篇的讨论范围之内.
- 0:000> !dso
- Listing objects from: 0000000000386000 to 0000000000390000 from thread: 0 [3f50]
- …..
- 0000000002a93f70 00007ffdb0817df0 0 0 66 System.String <strong>SimpleString method</strong>
- 0000000002a93fb8 00007ffdb0817df0 0 0 52 System.String <strong>Concat String
- </strong>0000000002a93ff0 00007ffdb0817df0 0 0 92 System.String <strong>SimpleString method Concat String</strong>
- 0000000002a97a90 00007ffdb0817df0 0 0 28 System.String <strong>0</strong>
- 0000000002a97ab0 00007ffdb0817df0 0 0 68 System.String <strong>SimpleString method 0</strong>
- ……
复制代码
我们可以检查一下这个System.Object[]里面都有什么.
从这个数组里面可以看到代码中显示声明的的字符串. 第一个元素是一个空值, 这个里面保留的是我们最常用的String.Empty的实例. 第二个元素是”Run GC.Collect()”. 这个在code的里面的main函数中. 当前还没有被执行到, 但是已经被JITed到了该数组中. 其他两个被显示定义的字符串也能够在这个数组中被找到. 另外可以确认的是, 拼接出来的字符串, 临时生成的字符串都没有在这里出现. 然而, 通过拼接出来的String并不在这个数组里面. 虽然拼接出来的String同样分配到了heap上面, 但是不会被收纳到数组中.- 0:000> !gcroot 0000000002a93f70
- Note: Roots found on stacks may be false positives. Run "!help gcroot" for
- more info.
- Scan Thread 0 OSTHread 81a0
- <strong>RSP:b9e9b8</strong>:Root:0000000002a93f70(System.String)
- Scan Thread 2 OSTHread 7370
- DOMAIN(0000000000C51880):HANDLE(Pinned):217e8:Root:<strong>0000000012a93030(System.Object[])-></strong>
- 0000000002a93f70(System.String)
复制代码
所以经过上面的观察, 可以得出的结论是驻留的String生命周期非常长. 那么, 在什么时候他才会被回收?
从上面gcroot的结果, 可以看到主流数组是被PINNED住. 而引用这个数组的App Domain 0000000000C51880.
用!dumpdomain -stat的命令将所有的app domain信息打印出来. 可以看到这个App Domain是我们代码运行的Domain (ConsoleApplication3.exe). 这个驻留数组是由CLR 来维护, 并且与当前的App Domain联系到一起. 所以, 理论上这些驻留数组的生命周期跟这个App Domain是一致的.
- 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:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.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:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
- String: : <strong>Run GC.Collect()</strong>
- 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:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
- String: <strong>SimpleString method</strong>
- 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:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
- String: <strong>Concat String</strong>
- 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 <<
复制代码
写在最后面
- String的恒定性. 字符串横定性是指一个字符串一经创建,就不可改变。那么也就是说当我们改变string值的时候,便会在托管堆上重新分配一块新的内存空间,而不会影响到原有的内存地址上所存储的值。
- 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
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |