昨天看群里在討論C 中的string駐留池,炒的火熱,幾輪下來理論一堆堆,但是在證據提供上都比較尷尬。雖然這東西很基礎,但比較好的回答也不是那麼容易,這篇我就以我能力範圍之內跟大家分享一下 一:無處不在的池 開發這麼多年,相信大家對‘池’ 這個概念都耳熟能詳了,連接池,線程池,對象池,還有這裡的駐留 ...
昨天看群里在討論C#中的string駐留池,炒的火熱,幾輪下來理論一堆堆,但是在證據提供上都比較尷尬。雖然這東西很基礎,但比較好的回答也不是那麼容易,這篇我就以我能力範圍之內跟大家分享一下
一:無處不在的池
開發這麼多年,相信大家對‘池’ 這個概念都耳熟能詳了,連接池,線程池,對象池,還有這裡的駐留池,池的存在就是為了復用為了共用,獨樂樂不如眾樂樂,畢竟一個字元串的生成和銷毀既浪費空間又浪費時間,還不如先養著。
1. 說說現象
通常我們臆想中是這麼認為的,定義幾個字元串變數,堆上就會分配幾個string對象,其實這底層有一種叫駐留池技術可以做到如果兩個字元串內容相同,那就在堆上只分配一個string對象,然後將引用地址分配給兩個字元串變數,這樣就可以大大降低了記憶體使用,如果用代碼表示就是下麵這樣。
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是否都指向了堆上對象的地址。
~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 = 0x0000024a21f22d48
和 0x000000ac0b7fed30 = 0x0000024a21f22d48
可以看到兩個局部變數的引用地址都是 0x0000024a21f22d48
,說明指向的都是一個堆對象,接下來再把堆上的內容打出來。
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在駐留池中存在,那麼就返回駐留池中命中內容的堆上引用地址,如果不存在,將新字元串插入駐留池中再返回堆上引用,先上一下代碼:
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
。
四: 總結
駐留池技術是個很