摘要 : 最近在博客園里面看到有人在討論 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 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:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.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里面, 還沒有從這個方法返回.
我們可以看到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:\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: : 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:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.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:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.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:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) String: ... [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: : Run GC.Collect() … [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: SimpleString method ... [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: 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:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll] ClassLoader: 000000000047f820 SecurityDescriptor: 000000000047f9a0 Module Name 00007ffdb03e1000 C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll
寫在最后面
- 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