昨天看群里在讨论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:dreamCsharpConsoleApp1ConsoleApp2Program.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:WINDOWSMicrosoft.NetassemblyGAC_64mscorlibv4.0_4.0.0.0__b77a5c561934e089mscorlib.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 <<










