透过WinDBG的视角看String

摘要 : 最近在博客园里面看到有人在讨论 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次, 而且地址不一样, 分别是00000000024776700000000002477688 .

所以, 在使用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 = 0;
    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

时间: 2024-10-24 18:50:56

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

3D 视角看 Go 并发编程

主题:3D 视角看 Go 并发编程 Overview 并发编程综述 Goroutine Channel & Select & waitGroup 消息传递模式 可视化工具 GoTrace 主讲师:PP 先后在百度.第四范式.蚂蚁金服工作,百度 Go Good Coder, 对分布式计算.离线/实时大数据处理有丰富的实战经验.乐于分享自己的技术和学习心得. 一.并发编程综述 串行执行 并发与并行 多核时代的并发编程 左图:p1, p2, p3 这 3 个线程运行在单核上,分时复用 CPU,是

换一个视角看事务 - 用&quot;Java语言&quot;写&quot;作文&quot;

前段时间在抽工作之空余,更加系统和深入的重新学习Java的一些技术知识. 最近也试着申请了一个专栏,对前段时间的一些收获和知识点做一个系统性的归纳回顾和总结. 昨天也是刚刚写完了关于Java中的各种基础语言要素的总结,总觉得少了点什么. 对基础语言要素的理解和使用,实际上是很重要的. 俗话说,一切伟大的行动和思想,都源于一个微不足道的开始. 而对于一门语言来说,熟练的掌握对其基础语言要素的理解和使用,就是这个"微不足道的开始" 可以这样说,一门语言的基础语言要素,就等同于是一门武功的内

4星|《剑桥中国经济史:古代到19世纪》:经济学视角看中国古代史

剑桥中国经济史:古代到19世纪 万志英教授对自青铜时代至20世纪初这3000年间中国经济发展各种制度的基础.延续以及中断进行了详尽而通俗的研究 作者是海外汉学家.全书从经济学的角度重新梳理了中国古代史,有不少独到的视角与细节.比如说对古代史的分期,与我见过的任何一本书的分期都不同,唐朝以安史之乱为节点被拦腰分成两节归入前后两个时期,前半部分划入从北魏成熟期开始的中国再统一时期,后半部分到北宋灭亡属于第一次经济转型期. 书中涉及到大量历史书上不太提及的经济学问题,比如人民收入水平,粮食价格,实际的

从dubbo处理视角看Netty处理网络传输原理 -- 粘包与拆包

如今,我们想要开发一个网络应用,那是相当地方便.不过就是引入一个框架,然后设置些参数,然后写写业务代码就搞定了. 写业务代码自然很重要,但是你知道: 你的数据是怎么来的吗?通过网络传输过来的呗. 你知道网络是通过什么方式传输过来的吗?光纤呗,TCP/IP协议呗. 看起来都难不住我们的同学们,但是,以上问题都不是我们关注的重点,我们今天要关注的是,TCP.IP协议是如何把数据传输到我们的应用服务器,而且准确地交到对应的业务代码手上的? 我们也不关注TCP协议的三次握手四次挥手,我们只需要确认一点,

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

前言 自从Oracle收购Sun之后,对Java收费或加强控制的尝试从未间断,谷歌与Oracle围绕Java API的官司也跌宕起伏.虽然Oracle只是针对Oracle JDK8的升级收费,并释放了OpenJDK一直开源这份善意,但是如果没有各个大非Oracle的JVM.JDK和众多其它基于JVM的语言,Oracle这份善意能维持到什么时候可不好说. 大厂要从JVM和JDK的层面早做打算,而广大中小企业,就只能先从Java语言的层面,先找到Oracle以外的备胎.自从被谷歌钦定为Android

Java 基础(四):从面试题看String

字符串介绍 String类是java.lang包中的一个类,是我们日常中使用的非常多的一个类,它不是基础数据类型,底层实现是字符数组来实现的: /** The value is used for character storage. */ private final char value[]; String类是由final修饰的,所以是无法被继承的,一旦创建了String对象,我们就无法改变它的值.因此,它是线程安全的,可以安全地用于多线程环境中. public final class Stri

旧题新做:从idy的视角看数据结构

“今天你不写总结……!!!” 额…… 还是讲我的吧.这些考试都是idy出的题. 20170121:DFS序. ST表.线段树练习 这是第一次考数据结构. Problem 1. setsum 1 second 给你一个长度为N 的整数序列,支持两种操作: • modity l r val 将区间[l,r] 中的所有数修改为val • query l r 询问区间[l,r] 所有数的和 分析:最简单的线段树,区间更改区间求和.但注意是更改,不是添改,sum与flag需同时覆盖. Problem 2.

另类的视角看“UIView”和“CALayer”——灵之于魄,男人之于肾的关系

清晨接连翻阅了几篇关于UIView和CALayer的博文,要么是上来一排排的代码(破坏了优雅的气氛),要么是题不达意(喂,what are you ‘写啥’ 嘞),看的我是雨里雾里,当然也有大牛辛辛苦苦的写的优秀文章,只是最近小编也正在学习“核心动画”,因为核心动画操作的对象不是UIView,而是CALayer,而对于二者那剪不断理还乱的关系 小编索性自己通过这篇博文整理了些粗浅的想法. -=-=-=-=-=-=-=--=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

从Java源码看String的两种比较方式

String的两种字符串比较方式 == 和 equals方法 ==: ==比较的是字符串在内存中的地址 代码示例: 1 public class EqualsDemo { 2 3 /** 4 * @param args 5 */ 6 public static void main(String[] args) { 7 String s1 = "String"; 8 String s2 = "String"; 9 String s3 = new String(&quo