很是簡單的string駐留池,你對它真的瞭解嗎

昨天看羣裏在討論C#中的string駐留池,炒的火熱,幾輪下來理論一堆堆,可是在證據提供上都比較尷尬。雖然這東西很基礎,但比較好的回答也不是那麼容易,這篇我就以我能力範圍以內跟你們分享一下線程

一:無處不在的池

開發這麼多年,相信你們對‘池’ 這個概念都耳熟能詳了,鏈接池,線程池,對象池,還有這裏的駐留池,池的存在就是爲了複用爲了共享,獨樂樂不如衆樂樂,畢竟一個字符串的生成和銷燬既浪費空間又浪費時間,還不如先養着。3d

1. 說說現象

一般咱們臆想中是這麼認爲的,定義幾個字符串變量,堆上就會分配幾個string對象,其實這底層有一種叫駐留池技術能夠作到若是兩個字符串內容相同,那就在堆上只分配一個string對象,而後將引用地址分配給兩個字符串變量,這樣就能夠大大下降了內存使用,若是用代碼表示就是下面這樣。code

public static void Main(string[] args)
        {
            var str1 = "nihao";
            var str2 = "nihao";

            var b = string.ReferenceEquals(str1, str2);
            Console.WriteLine(b);
        }

----------- output -----------
True

2. 實現原理

那怎麼作到的呢? 其實CLR在運行時調用JIT把你的MSIL代碼轉成機器代碼的時候會發現你的元數據中定義了相同內容的字符串對象,CLR就會把你的字符串放入它私有的的內部字典中,其中key就是字符串內容,value就是分配在堆上的字符串引用地址,這個字典就是所謂的駐留池,若是不是很明白,我來畫一張圖。對象

3. windbg驗證

能夠用windbg看一下棧中的str1和str2是否都指向了堆上對象的地址。blog

~0s -> !clrstack -l 在主線程的線程棧上找到變量str1和str2內存

0:000> ~0s
ntdll!ZwReadFile+0x14:
00007ff8`fea4aa64 c3              ret
0:000> !clrstack -l
OS Thread Id: 0x1c1c (0)
        Child SP               IP Call Site

000000ac0b7fed00 00007ff889e608e9 *** WARNING: Unable to verify checksum for ConsoleApp2.exe
ConsoleApp2.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp2\Program.cs @ 30]
    LOCALS:
        0x000000ac0b7fed38 = 0x0000024a21f22d48
        0x000000ac0b7fed30 = 0x0000024a21f22d48

000000ac0b7fef48 00007ff8e9396c93 [GCFrame: 000000ac0b7fef48]

從上面代碼的 LOCALS 的 0x000000ac0b7fed38 = 0x0000024a21f22d480x000000ac0b7fed30 = 0x0000024a21f22d48能夠看到兩個局部變量的引用地址都是 0x0000024a21f22d48,說明指向的都是一個堆對象,接下來再把堆上的內容打出來。ci

0:000> !do 0x0000024a21f22d48
Name:        System.String
MethodTable: 00007ff8e7a959c0
EEClass:     00007ff8e7a72ec0
Size:        36(0x24) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      nihao
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ff8e7a985a0  4000281        8         System.Int32  1 instance                5 m_stringLength
00007ff8e7a96838  4000282        c          System.Char  1 instance               6e m_firstChar
00007ff8e7a959c0  4000286       d8        System.String  0   shared           static Empty
                                 >> Domain:Value  0000024a203d41c0:NotInit  <<

能夠看到,果真是System.String對象,這就和個人圖是相符的。開發

二 駐留池的驗證

1. String下的駐留池驗證方法

很遺憾的是水平有限,因爲駐留池既不在堆中也不在棧上,目前還不知道怎麼用windbg去打印CLR中駐留池字典內容,不過也能夠經過 string.Intern 去驗證。字符串

//
        // Summary:
        //     Retrieves the system's reference to the specified System.String.
        //
        // Parameters:
        //   str:
        //     A string to search for in the intern pool.
        //
        // Returns:
        //     The system's reference to str, if it is interned; otherwise, a new reference
        //     to a string with the value of str.
        //
        // Exceptions:
        //   T:System.ArgumentNullException:
        //     str is null.
        [SecuritySafeCritical]
        public static String Intern(String str);

從註釋中能夠看到,這個方法的意思就是:若是你定義的str在駐留池中存在,那麼就返回駐留池中命中內容的堆上引用地址,若是不存在,將新字符串插入駐留池中再返回堆上引用,先上一下代碼:string

public static void Main(string[] args)
        {
            var str1 = "nihao";
            var str2 = "nihao";

            //驗證nihao是否在駐留池中,若是存在那麼str3 和 str1,str2同樣的引用
            var str3 = string.Intern("nihao");

            //驗證新的字符串內容是否進入駐留池中
            var str4 = string.Intern("cnblogs");
            var str5 = string.Intern("cnblogs");

            Console.ReadLine();
        }

接下來分別驗證一下str3是否也是和str1和str2同樣的引用,以及str5是否存在駐留池中。

ConsoleApp2.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp2\Program.cs @ 37]
    LOCALS:
        0x00000047105fea58 = 0x0000018537312d48
        0x00000047105fea50 = 0x0000018537312d48
        0x00000047105fea48 = 0x0000018537312d48
        0x00000047105fea40 = 0x0000018537312d70
        0x00000047105fea38 = 0x0000018537312d70

從五個變量地址中能夠看到,nihao已經被str1,str2,str3共享,cnblogs也進入了駐留池中實現了共享。

2. 運行期相同string是否進入駐留池

這裏面有一個坑,前面討論的相同字符串都是在編譯期就知道的,但運行時中的相同字符串是否也會進入駐留池呢? 這是一個讓人充滿好奇的話題,能夠試一下,在程序運行時接受IO輸入內容hello,看看是否和str1,str2共享引用地址。

public static void Main(string[] args)
       {
           var str1 = "nihao";
           var str2 = "nihao";

           var str3 = Console.ReadLine();

           Console.WriteLine("輸入完成!");
           Console.ReadLine();
       }

0:000> !clrstack -l
000000f6d35fee50 00007ff889e7090d *** WARNING: Unable to verify checksum for ConsoleApp2.exe
ConsoleApp2.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp2\Program.cs @ 33]
   LOCALS:
       0x000000f6d35fee98 = 0x000002cb1a552d48
       0x000000f6d35fee90 = 0x000002cb1a552d48
       0x000000f6d35fee88 = 0x000002cb1a555f28
0:000> !do 0x000002cb1a555f28
Name:        System.String
MethodTable: 00007ff8e7a959c0
EEClass:     00007ff8e7a72ec0
Size:        36(0x24) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      nihao
Fields:
             MT    Field   Offset                 Type VT     Attr            Value Name
00007ff8e7a985a0  4000281        8         System.Int32  1 instance                5 m_stringLength
00007ff8e7a96838  4000282        c          System.Char  1 instance               6e m_firstChar
00007ff8e7a959c0  4000286       d8        System.String  0   shared           static Empty
                                >> Domain:Value  000002cb18ad39f0:NotInit  <<

從上面內容能夠看到,從Console.ReadLine接收到的引用地址是 0x000002cb1a555f28 ,雖然是相同內容,但卻沒有使用駐留池,這是由於駐留池在JIT靜態解析期就已經解析完成了,也就沒法享受複用之優,若是還想複用的話,在 Console.ReadLine() 包一層 string.Intern便可,以下所示:

public static void Main(string[] args)
        {
            var str1 = "nihao";
            var str2 = "nihao";

            var str3 = string.Intern(Console.ReadLine());

            Console.WriteLine("輸入完成!");
            Console.ReadLine();
        }

ConsoleApp2.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp2\Program.cs @ 33]
    LOCALS:
        0x0000008fac1fe9c8 = 0x000001ff46582d48
        0x0000008fac1fe9c0 = 0x000001ff46582d48
        0x0000008fac1fe9b8 = 0x000001ff46582d48

能夠看到這個時候str1,str2,str3共享一個內存地址 0x000001ff46582d48

四: 總結

駐留池技術是個很🐮👃的東西,很好的解決字符串在堆上的重複分配問題,大大減少了堆的內存佔用,但也要明白運行期的IO輸入沒法共享駐留池的解決方案。

好了,本篇就說到這裏,但願對你有幫助!


如您有更多問題與我互動,掃描下方進來吧~


相關文章
相關標籤/搜索