浅谈C#中的string驻留池

 更新时间:2020年06月24日 14:24:07   作者:一线码农  
这篇文章主要介绍了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

四: 总结

驻留池技术是个很🐮👃的东西,很好的解决字符串在堆上的重复分配问题,大大减小了堆的内存占用,但也要明白运行期的IO输入无法共享驻留池的解决方案。

好了,本篇就说到这里,希望对你有帮助!

以上就是浅谈C#中的string驻留池的详细内容,更多关于C# string驻留池的资料请关注脚本之家其它相关文章!

相关文章

  • C#异步绑定数据实现方法

    C#异步绑定数据实现方法

    这篇文章主要介绍了C#异步绑定数据实现方法,实例分析了C#操作数据库及异步绑定的相关实现技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-09-09
  • .net与javascript脚本的交互方法总结

    .net与javascript脚本的交互方法总结

    这篇文章主要介绍了.net与javascript脚本的交互方法,实例讲述了.net访问javascript的方法与javascript访问.net的方法,非常具有实用价值,需要的朋友可以参考下
    2014-10-10
  • C#实现折半查找算法

    C#实现折半查找算法

    这篇文章介绍了C#实现折半查找的算法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-08-08
  • C#获取属性的displayName的3种方式

    C#获取属性的displayName的3种方式

    在C#中,获取属性的displayName可以通过多种方式实现,包括使用特性、反射和LINQ,下面我将分别展示每种方法,并提供具体的示例代码,感兴趣的朋友可以参考下
    2024-07-07
  • C# 使用Winform 获取下拉框 选中的值

    C# 使用Winform 获取下拉框 选中的值

    这篇文章主要介绍了C# 使用Winform 获取下拉框 选中的值,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-01-01
  • 利用C#操作WMI指南

    利用C#操作WMI指南

    WMI提供了一套内置在Microsoft Windows操作系统中的丰富的系统管理服务,可以在有大量的应用程序、服务和设备的系统中提供全方位的管理功能。它允许应用程序的开发者,使用简单的、一致的机制,去查询企业中的任一台计算机上的信息,或是进行系统配置
    2016-11-11
  • Unity Shader模拟玻璃效果

    Unity Shader模拟玻璃效果

    这篇文章主要为大家详细介绍了Unity Shader模拟玻璃效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-05-05
  • C# Record关键字示例详解

    C# Record关键字示例详解

    C#9.0引入record关键字,用于定义不可变且专注于数据表示的记录类型,记录类型自动生成构造函数、ToString、Equals和GetHashCode方法,并支持不可变性和可变性,适用于数据传输对象、不可变数据结构和配置对象等场景,本文介绍C# Record关键字,感兴趣的朋友一起看看吧
    2025-01-01
  • 基于C#实现图像缩放与裁剪工具

    基于C#实现图像缩放与裁剪工具

    图像缩放与裁剪在多个领域和应用场景中都非常常见,本文将介绍如何使用C#的GDI+(Graphics Device Interface)库来实现一个图像缩放与裁剪工具,有需要的可以参考下
    2025-03-03
  • C#实现缩放和剪裁图片的方法示例

    C#实现缩放和剪裁图片的方法示例

    这篇文章主要介绍了C#实现缩放和剪裁图片的方法,结合实例形式分析了C#针对图片属性的缩放、裁剪等相关操作技巧,需要的朋友可以参考下
    2017-06-06

最新评论