摘要 : 最近在博客园里面看到有人在讨论 C# String的一些特性. 大部分情况下是从CODING的角度来讨论String. 本人觉得非常好奇, 在运行时态, String是如何与这些特性联系上的. 本文将侧重在通过WinDBG来观察String在进程内的布局, 以此来解释C# String的一些特性.

问题

C# String有两个比较有趣的特性.

  1. String的恒定性. 字符串横定性是指一个字符串一经创建,就不可改变。那么也就是说当我们改变string值的时候,便会在托管堆上重新分配一块新的内存空间,而不会影响到原有的内存地址上所存储的值。
  2. String的驻留. CLR runtime通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统中只有一个。

对应着两个特性, 我产生了一些疑问.

  • String的恒定性是怎么样让string进行比较的时候出现有趣的结果的? 它的比较结果为什么会与其他引用类型的结果不一样?
  • 什么样的String会被放到拘留池中?
  • 拘留池是怎样的数据结构? 它真是个Hashtable吗?
  • 驻留在拘留池内的String会不会被GC,  它的生命周期会有多长(什么时候才会被回收)?

String的恒定性

先看一下下面的例子 :

private static void Comparation()
{
string a = "Test String";
string b = "Test String";
string c = a; Console.WriteLine("a vs b : " + object.ReferenceEquals(a, b));
Console.WriteLine("a vs c : " + object.ReferenceEquals(a, c)); SimpleObject smp1 = new SimpleObject(a);
SimpleObject smp2 = new SimpleObject(a); Console.WriteLine("smp1 vs smp2 : " + object.ReferenceEquals(smp1, smp2));
Console.ReadLine(); } class SimpleObject
{
public string name = string.Empty; public SimpleObject(string name)
{
this.name = name;
}
}

从结果上看, 虽然是不同的变量 a, b, c. 由于字符串的内容是相同的, 所以比较的结果也是完全相同的. 对比SimpleObject的实例, smp1和smp2的值虽然也是相同的,但是比较的结果为false.

下面看一下运行时, 这些objects的的情况.

在运行时态, 一切皆是地址. 判断两个变量是否是相同的对象, 直观的可以从它地址是否是相同的地址来进行判断.

用dso命令打印出栈上对应的Objects. 可以看到Test String”虽然出现了3次, 但是他们都对应了一个地址0000000002473f90 . SimpleObject的对象实例出现了2次, 而且地址不一样, 分别是0000000002477688 .

所以, 在使用String的时候, 实质上是重用了相同的String 对象. 在new一个SimpleObject的实例时候, 每一次new都会在新的地址上初始化该对象的结构. 每次都是一个新的对象.

0:000> !dso
OS Thread Id: 0x3f0c (0)
RSP/REG Object Name
...... 000000000043e730 0000000002473f90 System.String
000000000043e738 0000000002473f90 System.String
000000000043e740 0000000002473f90 System.String
000000000043e748 0000000002477670 ConsoleApplication3.SimpleObject
000000000043e750 0000000002477688 ConsoleApplication3.SimpleObject
....... 0:000> !do 0000000002473f90
Name: System.String
MethodTable: 00007ffdb0817df0
EEClass: 00007ffdb041e560
Size: 48(0x30) bytes
GC Generation: 0
(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: Test String
Fields:
MT Field Offset Type VT Attr Value Name
00007ffdb081f060 4000096 8 System.Int32 1 instance 12 m_arrayLength
00007ffdb081f060 4000097 c System.Int32 1 instance 11 m_stringLength
00007ffdb0819838 4000098 10 System.Char 1 instance 54 m_firstChar
00007ffdb0817df0 4000099 20 System.String 0 shared static Empty
>> Domain:Value 0000000000581880:0000000002471308 <<
00007ffdb08196e8 400009a 28 System.Char[] 0 shared static WhitespaceChars
>> Domain:Value 0000000000581880:0000000002471be0 <<

当字符串内容发生改变的时候, 任何微小的变化都会重新创建出一个新的String对象. 在我们调用这段代码的时候

Console.WriteLine("a vs b : " + object.ReferenceEquals(a, b));

CLR runtime实际上做了两件事情. 为字符"a vs b"分配了到了一个新的地址. 将对比结果与刚才的字符拼接到了一起, 分配到了另外一个新的地址. 如果多次拼接字符串, 就会分配到更多的新地址上, 从而可能会快速的占用大量的虚拟内存. 这就是为什么微软建议在这种情况下使用StringBuilder的原因.

0:000> !dso

Listing objects from: 0000000000435000 to 0000000000440000 from thread: 0 [3f0c]

Address          Method Table    Heap Gen      Size Type
…..
0000000002473fc0 00007ffdb0817df0 0 0 44 System.String a vs b :
0000000002474138 00007ffdb0817df0 0 0 52 System.String a vs b : True …..

String的驻留

CLR runtime通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统中只有一个。 我们看一下如何来理解这句话.

下面是示例代码 :

static void Main(string[] args)
{
int i = ;
while (true)
{
SimpleString(i++); Console.WriteLine( i + " : Run GC.Collect()");
GC.Collect();
Console.ReadLine();
}
} private static void SimpleString(int i)
{
string s = "SimpleString method ";
string c = "Concat String"; Console.WriteLine(s + c);
Console.WriteLine(s + i.ToString());
Console.ReadLine();
}

这是第一次的执行结果. 此时只执行到了SimpleString里面, 还没有从这个方法返回.

我们可以看到stack上有4个string. 分别是按照代码逻辑拼接起来的string的内容. 从这里我们就可以当我们在拼接字符串的时候, 实际上会在Heap上创建出多个String的对象, 以此来完成这个拼接动作.

0:000> !dso

Listing objects from: 0000000000386000 to 0000000000390000 from thread: 0 [3f50]

…..
0000000002a93f70 00007ffdb0817df0 0 0 66 System.String SimpleString method
0000000002a93fb8 00007ffdb0817df0 0 0 52 System.String Concat String
0000000002a93ff0 00007ffdb0817df0 0 0 92 System.String SimpleString method Concat String
0000000002a97a90 00007ffdb0817df0 0 0 28 System.String 0
0000000002a97ab0 00007ffdb0817df0 0 0 68 System.String SimpleString method 0 ……

随意用其中一个来检查它的引用情况.

从!gcroot的结果看, 这个string被两个地方引用到. 一个是当前的线程. 因为正在被当前线程使用到, 所以能够看到这个非常正常.

另外一个是root在一个System.Object[]数组上. 这个数组被PINNED在了App Domain 0000000000491880 上面. 这里显示出来, String其实是驻留在一个System.Object[]上面, 而不是很多人猜测的Hashtable. 不过料想CLR 应该有一套机制可以从这个数组中快速的获取正确的String. 不过这点不在本篇的讨论范围之内.

0:000> !gcroot 0000000002a93f70
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 81a0
RSP:b9e9b8:Root:0000000002a93f70(System.String)
Scan Thread 2 OSTHread 7370
DOMAIN(0000000000C51880):HANDLE(Pinned):217e8:Root:0000000012a93030(System.Object[])->
0000000002a93f70(System.String)

我们可以检查一下这个System.Object[]里面都有什么.

从这个数组里面可以看到代码中显示声明的的字符串. 第一个元素是一个空值, 这个里面保留的是我们最常用的String.Empty的实例. 第二个元素是”Run GC.Collect()”. 这个在code的里面的main函数中. 当前还没有被执行到, 但是已经被JITed到了该数组中. 其他两个被显示定义的字符串也能够在这个数组中被找到. 另外可以确认的是, 拼接出来的字符串, 临时生成的字符串都没有在这里出现. 然而, 通过拼接出来的String并不在这个数组里面. 虽然拼接出来的String同样分配到了heap上面, 但是不会被收纳到数组中.

0:000> !dumparray -details 0000000012a93030
Name: System.Object[]
MethodTable: 00007ffdb0805be0
EEClass: 00007ffdb041eb88
Size: 1056(0x420) bytes
Array: Rank 1, Number of elements 128, Type CLASS
Element Methodtable: 00007ffdb08176e0
[0] 0000000002a91308
Name: System.String
MethodTable: 00007ffdb0817df0
EEClass: 00007ffdb041e560
Size: 26(0x1a) bytes
(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String:
Fields:
MT Field Offset Type VT Attr Value Name
00007ffdb081f060 4000096 8 System.Int32 1 instance 1 m_arrayLength
00007ffdb081f060 4000097 c System.Int32 1 instance 0 m_stringLength
00007ffdb0819838 4000098 10 System.Char 1 instance 0 m_firstChar
00007ffdb0817df0 4000099 20 System.String 0 shared static Empty
>> Domain:Value 0000000000c51880:0000000002a91308 <<
00007ffdb08196e8 400009a 28 System.Char[] 0 shared static WhitespaceChars
>> Domain:Value 0000000000c51880:0000000002a91be0 <<
[1] 0000000002a93f30
Name: System.String
MethodTable: 00007ffdb0817df0
EEClass: 00007ffdb041e560
Size: 64(0x40) bytes
(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: : Run GC.Collect()
Fields:
MT Field Offset Type VT Attr Value Name
00007ffdb081f060 4000096 8 System.Int32 1 instance 20 m_arrayLength
00007ffdb081f060 4000097 c System.Int32 1 instance 19 m_stringLength
00007ffdb0819838 4000098 10 System.Char 1 instance 20 m_firstChar
00007ffdb0817df0 4000099 20 System.String 0 shared static Empty
>> Domain:Value 0000000000c51880:0000000002a91308 <<
00007ffdb08196e8 400009a 28 System.Char[] 0 shared static WhitespaceChars
>> Domain:Value 0000000000c51880:0000000002a91be0 <<
[2] 0000000002a93f70
Name: System.String
MethodTable: 00007ffdb0817df0
EEClass: 00007ffdb041e560
Size: 66(0x42) bytes
(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: SimpleString method
Fields:
MT Field Offset Type VT Attr Value Name
00007ffdb081f060 4000096 8 System.Int32 1 instance 21 m_arrayLength
00007ffdb081f060 4000097 c System.Int32 1 instance 20 m_stringLength
00007ffdb0819838 4000098 10 System.Char 1 instance 53 m_firstChar
00007ffdb0817df0 4000099 20 System.String 0 shared static Empty
>> Domain:Value 0000000000c51880:0000000002a91308 <<
00007ffdb08196e8 400009a 28 System.Char[] 0 shared static WhitespaceChars
>> Domain:Value 0000000000c51880:0000000002a91be0 <<
[3] 0000000002a93fb8
Name: System.String
MethodTable: 00007ffdb0817df0
EEClass: 00007ffdb041e560
Size: 52(0x34) bytes
(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: Concat String
Fields:
MT Field Offset Type VT Attr Value Name
00007ffdb081f060 4000096 8 System.Int32 1 instance 14 m_arrayLength
00007ffdb081f060 4000097 c System.Int32 1 instance 13 m_stringLength
00007ffdb0819838 4000098 10 System.Char 1 instance 43 m_firstChar
00007ffdb0817df0 4000099 20 System.String 0 shared static Empty
>> Domain:Value 0000000000c51880:0000000002a91308 <<
00007ffdb08196e8 400009a 28 System.Char[] 0 shared static WhitespaceChars
>> Domain:Value 0000000000c51880:0000000002a91be0 <<

继续让代码执行下去, 我们需要来几次GC. 验证一下驻留的字符串是否会在不使用之后被GC掉.

GC完成之后, 按照所设想的, CallStack上面的String都已经被清除掉了.同时因为已经做过了GC动作, GC heap进过了压缩, 没有被PINNED住的对象地址会发生改变. 所以要验证驻留的String是否会被回收, 可以从驻留数组下手. 由于该数组是被PINNED住, 所以即使发生了GC的动作, 它的地址也不会发生改变. 所以可以通过相同的命令把数组里面驻留的String都列出来.

结果是与我的预期是一致的. 只有被显示定义的String保留在该数组内, 而这些String不会被回收. 通过拼接零时生产的String, 则不会加入到这个数组内, 在GC发生后, 由于没有被引用而被回收掉.

0:000> !dumparray -details 0000000012a93030
Name: System.Object[]
MethodTable: 00007ffdb0805be0
EEClass: 00007ffdb041eb88
Size: 1056(0x420) bytes
Array: Rank 1, Number of elements 128, Type CLASS
Element Methodtable: 00007ffdb08176e0
[0] 0000000002a91308
Name: System.String
MethodTable: 00007ffdb0817df0
EEClass: 00007ffdb041e560
Size: 26(0x1a) bytes
(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String:
...
[1] 0000000002a93f30
Name: System.String
MethodTable: 00007ffdb0817df0
EEClass: 00007ffdb041e560
Size: 64(0x40) bytes
(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: : Run GC.Collect()
[2] 0000000002a93f70
Name: System.String
MethodTable: 00007ffdb0817df0
EEClass: 00007ffdb041e560
Size: 66(0x42) bytes
(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: SimpleString method
...
[3] 0000000002a93fb8
Name: System.String
MethodTable: 00007ffdb0817df0
EEClass: 00007ffdb041e560
Size: 52(0x34) bytes
(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: Concat String

所以经过上面的观察, 可以得出的结论是驻留的String生命周期非常长. 那么, 在什么时候他才会被回收?

从上面gcroot的结果, 可以看到主流数组是被PINNED住. 而引用这个数组的App Domain 0000000000C51880.

用!dumpdomain -stat的命令将所有的app domain信息打印出来. 可以看到这个App Domain是我们代码运行的Domain (ConsoleApplication3.exe). 这个驻留数组是由CLR 来维护, 并且与当前的App Domain联系到一起. 所以, 理论上这些驻留数组的生命周期跟这个App Domain是一致的.

0:000> !dumpdomain -stat
--------------------------------------
System Domain: 00007ffdb1f16f60
LowFrequencyHeap: 00007ffdb1f16fa8
HighFrequencyHeap: 00007ffdb1f17038
StubHeap: 00007ffdb1f170c8
Stage: OPEN
Name: None
--------------------------------------
Shared Domain: 00007ffdb1f17860
LowFrequencyHeap: 00007ffdb1f178a8
HighFrequencyHeap: 00007ffdb1f17938
StubHeap: 00007ffdb1f179c8
Stage: OPEN
Name: None
Assembly: 000000000047fa60
--------------------------------------
Domain 1: 0000000000491880
LowFrequencyHeap: 00000000004918c8
HighFrequencyHeap: 0000000000491958
StubHeap: 00000000004919e8
Stage: OPEN
SecurityDescriptor: 0000000000494140
Name: ConsoleApplication3.exe
Assembly: 000000000047fa60 [C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll]
ClassLoader: 000000000047f820
SecurityDescriptor: 000000000047f9a0
Module Name
00007ffdb03e1000 C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll

写在最后面

  1. String的恒定性. 字符串横定性是指一个字符串一经创建,就不可改变。那么也就是说当我们改变string值的时候,便会在托管堆上重新分配一块新的内存空间,而不会影响到原有的内存地址上所存储的值。
  2. String的驻留. CLR runtime通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统(App Domain)中只有一个。
    直接在CODE里面声明的String会被CLR runtime维护在一个Object[]内.
    临时生成的string或者拼接出来的String不会维护在这个驻留数组中.
    驻留数组的生命周期跟它位于的App Domain一样长. 所以GC并不会影响驻留数组所引用的String, 它们不会被GC.

可以参考下面这个链接来对这两个特性加深理解.

http://blog.csdn.net/fengshi_sh/article/details/14837445

http://www.cnblogs.com/charles2008/archive/2009/04/12/1434115.html

http://www.cnblogs.com/instance/archive/2011/05/24/2056091.html

透过WinDBG的视角看String的更多相关文章

  1. Mybatis系列全解(七):全息视角看Dao层两种实现方式之传统方式与代理方式

    封面:洛小汐 作者:潘潘 一直以来 他们都说为了生活 便追求所谓成功 顶级薪水.名牌包包 还有学区房 · 不过 总有人丢了生活 仍一无所获 · 我比较随遇而安 有些事懒得明白 平日里问心无愧 感兴趣的 ...

  2. 当要将其他类型转成String类型时候 看String的方法

    当要将其他类型转成String类型时候 看String的方法进行转换

  3. 换一种视角看DNS(采坑篇)

    换一种视角看DNS 我们尽量用精炼的语言,尽可能的规划DNS的全貌(当然笔者水平有限,如有错误请不吝赐教). 通常啊我们在个人PC中能看到DNS的配置身影就是在上网的时候,通常如果你不配置DNS可能找 ...

  4. 从.Net版本演变看String和StringBuild性能之争

    在C#中string关键字的映射实际上指向.NET基类System.String.System.String是一个功能非常强大且用途非常广泛的基类,所以我们在用C#string的时候实际就是在用.NE ...

  5. 从源码看String,StringBuffer,StringBuilder的区别

    前言 看了一篇文章,大概是讲面试中的java基础的,有如题这么个面试题.我又翻了一些文章看了下,然后去看源码.看一下源码大概能更加了解一些. String String类是final的,表示不可被继承 ...

  6. 从.Net版本演变看String和StringBuilder性能之争

    在C#中string关键字的映射实际上指向.NET基类System.String.System.String是一个功能非常强大且用途非常广泛的基类,所以我们在用C#string的时候实际就是在用.NE ...

  7. 从架构师视角看是否该用Kotlin做服务端开发?

    前言 自从Oracle收购Sun之后,对Java收费或加强控制的尝试从未间断,谷歌与Oracle围绕Java API的官司也跌宕起伏.虽然Oracle只是针对Oracle JDK8的升级收费,并释放了 ...

  8. 工作10年后,再看String s = new String("xyz") 创建了几个对象?

    这个问题相信每个学习java的同学都不陌生,作为一个经典的面试题,到现在工作这么多年了我真是认为挺操蛋的一个问题,在网上到现在你仍然可以看见很多讨论这个问题的人,其中不乏工作很多年的人都有争论,我认为 ...

  9. 用户视角 vs 系统视角 看性能

    如何评价性能的优劣: 用户视角 vs. 系统视角 对于最终用户(End-User)来说,评价系统的性能好坏只有一个字——“快”.最终用户并不需要关心系统当前的状态——即使系统这时正在处理着成千上万的请 ...

随机推荐

  1. 使用Monit监控本地进程

    目前用它监控某些服务,失败自动重启,同时监控特定的日志文件,如果有变化,就发邮件报警 安装不细写了,网上好多 我先用cat /proc/version看了下我的系统是el6的,于是wget http: ...

  2. python爬取github数据

    爬虫流程 在上周写完用scrapy爬去知乎用户信息的爬虫之后,github上star个数一下就在公司小组内部排的上名次了,我还信誓旦旦的跟上级吹牛皮说如果再写一个,都不好意思和你再提star了,怕你们 ...

  3. DDD 领域驱动设计-商品建模之路

    最近在做电商业务中,有关商品业务改版的一些东西,后端的架构设计采用现在很流行的微服务,有关微服务的简单概念: 微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成.系统中的各个微服务可被独 ...

  4. 快递Api接口 & 微信公众号开发流程

    之前的文章,已经分析过快递Api接口可能被使用的需求及场景:今天呢,简单给大家介绍一下微信公众号中怎么来使用快递Api接口,来完成我们的需求和业务场景. 开发语言:Nodejs,其中用到了Neo4j图 ...

  5. CentOS下mysql数据库常用命令总结

    mysql数据库使用总结 本文主要记录一些mysql日常使用的命令,供以后查询. 1.更改root密码 mysqladmin -uroot password 'yourpassword' 2.远程登陆 ...

  6. jdb调试scala代码的简单介绍

    在linux调试C/C++的代码需要通过gdb,调试java代码呢?那就需要用到jdb工具了.关于jdb的用法在网上大家都可以找到相应的文章,但是对scala进行调试的就比较少了.其实调试的大致流程都 ...

  7. 网站里加入QQ在线客服

    1.开启"QQ在线状态"服务  http://jingyan.baidu.com/article/b24f6c823425a586bfe5da1f.html http://www. ...

  8. DockerCon 2016 – 微软带来了什么?

    根据Forrester的调查,接近半数的企业CIO在考虑IT架构的时候更乐于接受开源方案,这主要是基于低成本,避免供应商锁定和敏捷的需求:同时另外一家North Bridge的调研机构的调查显示,20 ...

  9. Windows 上安装 Jekyll.

    Jekyll是一个静态网站生成工具.它允许用户使用HTML.Markdown或Textile来建立静态页面,然后通过模板引擎Liquid(Liquid Templating Engine)来运行. 原 ...

  10. 《徐徐道来话Java》:PriorityQueue和最小堆

    在讲解PriorityQueue之前,需要先熟悉一个有序数据结构:最小堆. 最小堆是一种经过排序的完全二叉树,其中任一非终端节点数值均不大于其左孩子和右孩子节点的值. 可以得出结论,如果一棵二叉树满足 ...