透過WinDBG的視角看String

摘要 : 最近在博客園裏面看到有人在討論 C# String的一些特性. 大部分狀況下是從CODING的角度來討論String. 本人以爲很是好奇, 在運行時態, String是如何與這些特性聯繫上的. 本文將側重在經過WinDBG來觀察String在進程內的佈局, 以此來解釋C# String的一些特性.html

 

問題

C# String有兩個比較有趣的特性.編程

  1. String的恆定性. 字符串橫定性是指一個字符串一經建立,就不可改變。那麼也就是說當咱們改變string值的時候,便會在託管堆上從新分配一塊新的內存空間,而不會影響到原有的內存地址上所存儲的值。
  2. String的駐留. CLR runtime經過維護一個表來存放字符串,該表稱爲拘留池,它包含程序中以編程方式聲明或建立的每一個惟一的字符串的一個引用。所以,具備特定值的字符串的實例在系統中只有一個。

對應着兩個特性, 我產生了一些疑問.windows

  • 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的的狀況. app

在運行時態, 一切皆是地址. 判斷兩個變量是不是相同的對象, 直觀的能夠從它地址是不是相同的地址來進行判斷. dom

用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:\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對象. 在咱們調用這段代碼的時候ui

    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:\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

 

寫在最後面

  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

相關文章
相關標籤/搜索