昨天看群里在讨论C#中的string驻留池,炒的火热,几轮下来理论一堆堆,可是在证据提供上都比较尴尬。虽然这东西很基础,但比较好的回答也不是那么容易,这篇我就以我能力范围以内跟你们分享一下线程
开发这么多年,相信你们对‘池’ 这个概念都耳熟能详了,链接池,线程池,对象池,还有这里的驻留池,池的存在就是为了复用为了共享,独乐乐不如众乐乐,毕竟一个字符串的生成和销毁既浪费空间又浪费时间,还不如先养着。3d
一般咱们臆想中是这么认为的,定义几个字符串变量,堆上就会分配几个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
那怎么作到的呢? 其实CLR在运行时调用JIT把你的MSIL代码转成机器代码的时候会发现你的元数据中定义了相同内容的字符串对象,CLR就会把你的字符串放入它私有的的内部字典中,其中key就是字符串内容,value就是分配在堆上的字符串引用地址,这个字典就是所谓的驻留池,若是不是很明白,我来画一张图。对象
能够用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 = 0x0000024a21f22d48
和 0x000000ac0b7fed30 = 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对象,这就和个人图是相符的。开发
很遗憾的是水平有限,因为驻留池既不在堆中也不在栈上,目前还不知道怎么用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也进入了驻留池中实现了共享。
这里面有一个坑,前面讨论的相同字符串都是在编译期就知道的,但运行时中的相同字符串是否也会进入驻留池呢? 这是一个让人充满好奇的话题,能够试一下,在程序运行时接受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输入没法共享驻留池的解决方案。
好了,本篇就说到这里,但愿对你有帮助!