字符串的驻留(String Interning)

关于字符串的驻留的机制,对于那些了解它的人肯定会认为很简单,但是我相信会有很大一部分人对它存在迷惑。在开始关于字符串的驻留之前,先给出一个有趣的Sample:

Code Snip:

static void Main(string[] args)
        
{
            
string str1 = "ABCD1234";
            
string str2 = "ABCD1234";
            
string str3 = "ABCD";
            
string str4 = "1234";
            
string str5 = "ABCD" + "1234";
            
string str6 = "ABCD" + str4;
            
string str7 = str3 + str4;

            Console.WriteLine(
"string str1 = \"ABCD1234\";");
            Console.WriteLine(
"string str2 = \"ABCD1234\";");
            Console.WriteLine(
"string str3 = \"ABCD\";");
            Console.WriteLine(
"string str4 = \"1234\";");
            Console.WriteLine(
"string str5 = \"ABCD\" + \"1234\";");
            Console.WriteLine(
"string str6 = \"ABCD\" + str4;");
            Console.WriteLine(
"string str7 = str3 + str4;");

            Console.WriteLine(
"\nobject.ReferenceEquals(str1, str2) = {0}"object.ReferenceEquals(str1, str2));
            Console.WriteLine(
"object.ReferenceEquals(str1,  \"ABCD1234\") = {0}"object.ReferenceEquals(str1, "ABCD1234"));

            Console.WriteLine(
"\nobject.ReferenceEquals(str1, str5) = {0}"object.ReferenceEquals(str1, str5));
            Console.WriteLine(
"object.ReferenceEquals(str1, str6) = {0}"object.ReferenceEquals(str1, str6));
            Console.WriteLine(
"object.ReferenceEquals(str1, str7) = {0}"object.ReferenceEquals(str1, str7));

            Console.WriteLine(
"\nobject.ReferenceEquals(str1, string.Intern(str6)) = {0}"object.ReferenceEquals(str1, string.Intern(str6)));
            Console.WriteLine(
"object.ReferenceEquals(str1, string.Intern(str7)) = {0}"object.ReferenceEquals(str1, string.Intern(str7)));
        }


下边是输出的结果:


接下来我们来逐句地分析这段代码:

首先我们创建了两个完全相同的字符串(ABCD1234),并将他们分别赋予了两个字符创变量——str1和str2。然后把它们传给了object.ReferenceEquals。我们知道object.ReferenceEquals是用于确定两个变量是否具有相同的引用——换句话说,当两个变量引用的是同一块托管推的内存快的时候,返回True,否则返回False。

string str1 = "ABCD1234";
string str2 = "ABCD1234";
object.ReferenceEquals(str1, str2)= True;
object.ReferenceEquals(str1, "ABCD1234")) = True;

令我们感到奇怪的是,当我们分别创建的引用类型两个变量——string是引用类型。照理说CLR会在托管堆(Managed Heap)中为它们分配两段内存快,他们不可能具有相同的引用才对,但是为什么object.ReferenceEquals 方法会返回True呢。而对于第二个比较——一个字符串变量和一个和他具有相同内容的字符串("ABCD1234";)直接进行比较,按照我们对CLR内存的分配的一般理解,应该是CLR首先会在托管堆中为这段字符串("ABCD1234")分配内存快,然后把相对应的引用传递给object.ReferenceEquals方法(由于分配在托管堆的这段字符串并没有被任何变量引用,所以当垃圾回收的时候会被回收掉),所以无论如何也不应该返回True。 

我们先把问题留到最后,接着分析我们的Sample。上面们对字符串变量之间以及变量与字符串之间进行了比较,如果我们对一个字符串变量和一个动态创建的字符串(通过+Operator把两个字符串连接起来)进行比较,结果又会如何呢?我们来看看下面的伪代码演示: 

string str3 = "ABCD";
string str4 = "1234";
string str5 = "ABCD" + "1234"
string str6 = "ABCD" + str4;
string str7 = str3 + str4;
object.ReferenceEquals(str1, str5) = True
object.ReferenceEquals(str1, str6) = False
object.ReferenceEquals(str1, str7)) = False

在上面的例子中,我们用三种不同的方式创建了3个字符串变量(str5,str6,str7)——string+string;string+variable;variable+variable。然后分别和我们已经创建的、和它们具有相同字符串“值”的变量(str1)作比较。同样令我们感到奇怪的是第一个返回True,而后两个则为False。带着这些疑惑我们来看看对于string这一特殊的类型说采用的特殊的使用机制。 

1. System.String虽然是一个引用类型,但是它具有其自身的特殊性。我们最容易想到的是它创建的特殊性——一般的对象在创建的时候需要通过new关键字调用对应的构造函数来实现;而创建一段string不需要这么做——我们只需要把对应的字符换赋给给对应的字符串变量就可以了。之所以存在着这种差异,是因为他们在创建过程中使用的IL指令时不同的——一般的引用对象的创建是通过newobj这样一个IL指令来实现的,而创建一个字符串变量的IL指令则是ldstr (load string)。(象C#,VB.NET这样的语言毕竟是高级语言,进行了高度的抽象,站在这样的角度分析问题往往不能够看到其实质,所以有时候我们把应该从交底层上面找突破口——比如分析IL,Metadata…);

2. 由于String是我们做到频率最高的一种类型,CLR考虑性能的提升和内存节约上,对于相同的字符串,一般不会为他们分别分配内存块,相反地,他们会共享一块内存。CLR实际上采用这个的机制来实现的:CLR内部维护着一块特殊的数据结构——我们可以把它看成是一个Hash table,这个Hash table维护者大部分创建的string(我这里没有说全部,因为有特例)。这个Hash table的Key对应的相应的string本身,而Value则是分配给这个string的内存块的引用。当CLR初始化的时候创建这个Hash table。一般地,在程序运行过程中,如果需要的创建一个string,CLR会根据这个string的Hash Code试着在Hash table中找这个相同的string,如果找到,则直接把找到的string的地址赋给相应的变量,如果没有则在托管堆中创建一个string,CLR会先在managed heap中创建该strng,并在Hash table中创建一个Key-Value Pair——Key为这个string本身,Value位这个新创建的string的内存地址,这个地址最重被赋给响应的变量。这样我们就能解释上面的疑问了。 

string str1 = "ABCD1234";
string str2 = "ABCD1234";
object.ReferenceEquals(str1, str2)= True;
object.ReferenceEquals(str1, "ABCD1234")) = True;

当创建str1的时候,CLR现在我们上面提到的Hash table中找“ABCD1234”这样的一个string,没有找到,则在托管堆中为这个string分配一块内存,然后在Hash table为该string添加一个Key-Value Pair。接着创建str2,CLR仍然会在Hash table找ABCD1234这样的一个string,这回它会找到我们新创建的这个Entry,所以这个Key-Value Pair中Value(string的地址)会赋给str2。因为str1和str2 具有相同的引用,所以调用object.ReferenceEquals返回True。同理当我们对str1和"ABCD1234"进行比较的时候,str1直接传入该方法,放传入"ABCD1234"这个字符串的时候,CLR同样会在Hash table找ABCD1234这样的一个string,相同的Entry被找到,这个Entry(Key-Value Pair)的Value(string的地址)被传到object.ReferenceEquals,所以他们仍然相同的引用,结果返回True。 

3. 并非所有的情况下字符串的驻留都会起作用。对于对一个动态创建的字符串(比如string+variable;variable+variable),这种驻留机制便不会起作用。因为对于这样的字符串,是不会被添加到内部的Hash table中的。但是对于string+string则不同,因为当这样的语句被编译成IL的时候,编译器是先把结构计算出来,然后再调用ldstr指令——而对于string+variable;variable+variable这种情况,所对应的IL指令是Concat。所以对于string+string字符串的驻留仍然有效。

比如对于以下一段代码:

  static void Main(string[] args)
        
{
            
string str1 = "ABC";
            
string str2 = str1 + "123";
            
string str3 = "ABC" + "123";
}

对应的IL Code是:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  
// Code size       26 (0x1a)
  .maxstack  2
  .locals init ([
0string str1,
           [
1string str2,
           [
2string str3)
  IL_0000:  nop
  IL_0001:  ldstr      
"ABC"
  IL_0006:  stloc.
0
  IL_0007:  ldloc.
0
  IL_0008:  ldstr      
"123"
  IL_000d:  call       
string [mscorlib]System.String::Concat(string,
                                                              
string)
  IL_0012:  stloc.
1
  IL_0013:  ldstr      
"ABC123"
  IL_0018:  stloc.
2
  IL_0019:  ret
}
 // end of method Program::Main


所以现在我们就可以解释第二个疑问了。

虽然对于对一个动态创建的字符串(比如string+variable;variable+variable),驻留机制便不会起作用。但是我们可以手工的启用驻留机制——那就是调用定义的System.String中的静态方法Intern。这个方法接受一个字符串作为他的输入参数,返回的经过驻留处理的string。他的实现机制是:如果能在内部的Hash Table中找到传入的string,则返回对应的string引用,否则就在Hash Table添加该string对应的Entry,并返回string的引用。所以下面的代码就不难解释了。

            Console.WriteLine("\nobject.ReferenceEquals(str1, string.Intern(str6)) = {0}"object.ReferenceEquals(str1, string.Intern(str6)));
            Console.WriteLine(
"object.ReferenceEquals(str1, string.Intern(str7)) = {0}"object.ReferenceEquals(str1, string.Intern(str7)));

 相关内容:
[原创]如何改Managed Code的Performance和Scalability系列之二:深入理解string和如何高效地使用string

posted @ 2007-03-04 23:03 Artech 阅读(16953) 评论(60) 编辑 收藏

 回复 引用 查看   
#1楼 2007-03-04 23:26 Cat Chen      
好文章,真的有很多人都不了解这个内部机制。
 回复 引用 查看   
#2楼 2007-03-05 00:04 Gavin.Lv.      
好文章
 回复 引用 查看   
#3楼 2007-03-05 00:32 Jeffrey Zhao      
能把这个问题解释清楚的人,相信它对于.NET Framework的理解已经能够胜任大多数使用.NET Framework进行开发的情况了。
 回复 引用 查看   
#4楼 2007-03-05 04:41 周奔驰      
这个Hash table维护者大部分创建的string(我这里没有说全部,因为有特例)
--什么特例?

 回复 引用 查看   
#5楼 2007-03-05 08:45 Icebird      
MSDN称之为字符串的留用,那个内部的hashtable称为拘留池


 回复 引用 查看   
#6楼[楼主] 2007-03-05 09:12 Artech      
@Icebird
对于string interning,MSDN解释为留用,但我觉得驻留更妥贴。

 回复 引用 查看   
#7楼[楼主] 2007-03-05 09:12 Artech      
@周奔驰
比如文章后面提到的比如string+variable;variable+variable情况。

 回复 引用 查看   
#8楼 2007-03-05 09:14 西门子乌      
非常好.
 回复 引用 查看   
#9楼 2007-03-05 10:36 idior      
不错,博客园又来一位不错的作者。 :)
 回复 引用 查看   
#10楼[楼主] 2007-03-05 10:51 Artech      
@idior
:)Thank you!

 回复 引用   
#11楼 2007-03-05 10:57 william[未注册用户]
这哥们的文章太强了,篇篇经典啊!

哥们,你的bolg我收藏了,每天必看啊

 回复 引用 查看   
#12楼[楼主] 2007-03-05 11:00 Artech      
@william
谢谢关注!

 回复 引用   
#13楼 2007-03-05 12:28 壁虎[未注册用户]
又多明白了一点。。。。谢谢。
 回复 引用 查看   
#14楼[楼主] 2007-03-05 12:41 Artech      
@壁虎
看到别人有所收获是一件很开心的事情!

 回复 引用   
#15楼 2007-03-05 13:08 yukaizhao[未注册用户]
强悍,受益匪浅。
 回复 引用   
#16楼 2007-03-05 15:12 BlueMountain[未注册用户]
强悍
 回复 引用 查看   
#17楼 2007-03-05 17:22 Anders Liu      
嗯,强文!
 回复 引用 查看   
#18楼 2007-03-05 19:11 reonlyrun      
不错,方法很巧妙,呵呵
 回复 引用 查看   
#19楼 2007-03-05 21:23 在北京的湖南人      
字符串池,就是
 回复 引用 查看   
#20楼[楼主] 2007-03-06 00:10 Artech      
@在北京的湖南人
字符串池是C和C++的概念。CLR对应的字符串池是指:在编译、生成元数据的过程中,为了避免把说所有的string都放在Assembly的Metadata中而引起的膨胀效应,CLR采用Pooling的机制——对应相同的string,CLR只会存储一次,而其他的地方引用该string。
不过从广义来看,可以把string Interning理解成一种string Pooling。

 回复 引用   
#21楼 2007-03-06 09:28 雨恨云愁[未注册用户]
实际上string在.Net的设计时是采用了享元模式
你可以从这点上再分析分析

 回复 引用 查看   
#22楼[楼主] 2007-03-06 09:31 Artech      
@雨恨云愁
不错哦,这是一个很好的切入点。以后在这个基础上再完善。
Thank U!

 回复 引用 查看   
#23楼 2007-04-17 10:29 Shark Xu      
很好。学了一招
 回复 引用 查看   
#24楼[楼主] 2007-04-18 19:01 Artech      
@Shark Xu
:)

 回复 引用   
#25楼 2007-05-08 10:14 ikaiser[未注册用户]
非常不错的文章
 回复 引用 查看   
#26楼[楼主] 2007-05-08 11:16 Artech      
@ikaiser
:)

 回复 引用 查看   
#27楼[楼主] 2007-05-08 18:38 Artech      
关于string interning的进一步的讨论可以参考:
深入理解string和如何高效地使用string

 回复 引用 查看   
#28楼 2007-05-25 11:03 阿牛      
答案和我想得一样。(自夸一下下)


 回复 引用 查看   
#29楼 2008-05-08 10:28 Flicker      
C# 的字符串驻留也就是Java中的字符串池的概念,要理解这个首先要理解字符串不可变的概念。
 回复 引用 查看   
#30楼 2009-02-11 16:21 阿鹏      
写的真的非常好 :)
虽然我晚了1年多才看到,不过还是有眼福。
不知道楼主能否推荐一些查阅IL的资源,对IL不熟悉。

 回复 引用 查看   
#31楼 2009-02-12 13:24 -brian-      
很不错, 看来楼主的文章要好好研究了
 回复 引用 查看   
#32楼[楼主] 2009-02-16 08:04 Artech      
@阿鹏
这里有一篇介绍IL指令的列表!

 回复 引用 查看   
#33楼[楼主] 2009-02-16 08:04 Artech      
@-brian-
谢谢!

 回复 引用   
#34楼 2009-02-17 16:22 jimnywu[未注册用户]
看了2遍了~去年一遍,今年一遍~感觉真的很舒服~楼主的文章真让人兴奋,太谢谢了~~
 回复 引用 查看   
#35楼[楼主] 2009-03-02 16:13 Artech      
--引用--------------------------------------------------
jimnywu: 看了2遍了~去年一遍,今年一遍~感觉真的很舒服~楼主的文章真让人兴奋,太谢谢了~~
--------------------------------------------------------
太给面子了, 呵呵!

 回复 引用 查看   
#36楼 2009-04-16 15:07 龙潜冰风悄林      
楼主 有几个不太清楚的地方想问下你:
1、关于堆的定义 好像记得堆有分几块 有点忘记了 希望楼主能提示下
2、对一个动态创建的字符串(比如string+variable;variable+variable),它是动态分配在托管堆里吗?? 如果是的话,手工驻留之后呢??

 回复 引用 查看   
#37楼 2009-04-16 15:31 龙潜冰风悄林      
楼主 能谈下 拘留池和GC,托管堆的关系不
 回复 引用 查看   
#38楼 2009-04-16 15:37 龙潜冰风悄林      
如:
static void Main(string[] args)
{
string str = "c";


string str3 = "ab";
string str2 = "ab" + str;

// Console.WriteLine(string.Intern(str2));//记号一
Console.WriteLine(string.IsInterned(str2));
Console.WriteLine(object.ReferenceEquals(str3, str2));

Console.ReadKey();
}
此时的str2在内存中是如何存放的,如果记号一语句开放,str2在内存中是如何存放的?又如果记号一语句改为string str="abc"并进行手工拘留,str2在内存中又是如何存放的?

 回复 引用 查看   
#39楼 2009-09-09 13:46 阳光de混      
楼主,请问下,为什么动态创建String时,驻留机制不起作用呢
 回复 引用 查看   
#40楼 2009-09-10 16:32 獨翏淚      
@阳光de混
因为在ldstr时候才会在Hash表里面查找键,返回值.
如果没有查找到,就新创建一个..
动态创建的会作为临时字符串处理,下次GC运行时就被回收

 回复 引用 查看   
#41楼 2009-09-16 15:40 stargazer      
一步步再学习。感谢Artech!看你的博文回味无穷。
 回复 引用 查看   
#42楼 2009-09-23 20:53 阳光de混      
@獨翏淚

谢谢

 回复 引用 查看   
#43楼[楼主] 2009-10-10 16:30 Artech      
引用stargazer:一步步再学习。感谢Artech!看你的博文回味无穷。

引用阳光de混:
@獨翏淚

谢谢

Thank you both!

 回复 引用 查看   
#44楼 2010-03-02 22:51 吉桂昕      
LZ 看了你的文章真的是感觉你真的是太N了在2007年就对这个驻留机制有如何深入的研究。其实让我产生这个疑惑的还是在今天园子里的一个面试的博文吸引了大家对string的“刨根问底”。
其中就是string s = "a"; 和string ss = "d"+"b";分配了几次内存空间。

如果按我的理解是:s分配了二次,1,s 在栈上有一次用于存放"a"的引用。2,在堆上存放了实际的"a"的变量值。
而对于ss如果不是我今天看IL,我肯定会说有4次内存的分配,1,在栈上存引用地址。2,在堆上存"d"。3,在堆上存"b"。4,最后是组成”db“的连接。因为字符串有一个恒不变的原理。而最终看IL发现,在编译时就已经把"d"+"b"生成了"db".所以ss其实也只是进行了2次内存的分配。

从上面又引生了一个问题那就是
string s2 = "q";
s2 += "w";
这是不说他分配了3次呢?1,在栈上存了s2的引用地址。2,在堆上分配了"q"的空间。3,在堆上分配了"qw"的空间呢?

还望LZ多多指点。

 回复 引用 查看   
#45楼[楼主] 2010-03-06 09:43 Artech      
引用吉桂昕:
LZ 看了你的文章真的是感觉你真的是太N了在2007年就对这个驻留机制有如何深入的研究。其实让我产生这个疑惑的还是在今天园子里的一个面试的博文吸引了大家对string的“刨根问底”。
其中就是string s = "a"; 和string ss = "d"+"b";分配了几次内存空间。

如果按我的理解是:s分配了二次,1,s 在栈上有一次用于存放"a"的引用。2,在堆上存放了实际的"a"的变量值。
而对于ss如果不是我今天看IL,我肯定会说有4次内存的分配,1,在栈上存引用地址。2,在堆上存"d"。3,在堆上存"b"。4,最后是组成”db“的连接。因为字符串有一个恒不变的原理。而最终看IL发现,在编译时就已经把"d"+"b"生成了"db".所以ss其实也只是进行了2次内存的分配。

从上面又引生了一个问题那就是
string s2 = "q";
s2 += "w";
这是不说他分配了3次呢?1,在栈上存了s2的引用地址。2,在堆上分配了"q"的空间。3,在堆上分配了"qw"的空间呢?

还望LZ多多指点。
可以参考这篇文章:http://www.cnblogs.com/artech/archive/2007/05/06/737130.html

 回复 引用 查看   
#46楼 2010-03-25 14:37 anyTom      
看了楼主的文章,明白了~感谢楼主
 回复 引用 查看   
#47楼[楼主] 2010-03-25 23:35 Artech      
引用anyTom:看了楼主的文章,明白了~感谢楼主

:)

 回复 引用 查看   
#48楼 2010-04-29 10:29 丛零      
之前我也纳闷:
“当我们分别创建的引用类型两个变量——string是引用类型。照理说CLR会在托管堆(Managed Heap)中为它们分配两段内存快,他们不可能具有相同的引用才对,但是为什么object.ReferenceEquals 方法会返回True呢。”

 回复 引用 查看   
#49楼 2010-04-29 10:35 丛零      
“象C#,VB.NET这样的语言毕竟是高级语言,进行了高度的抽象,站在这样的角度分析问题往往不能够看到其实质,所以有时候我们把应该从交底层上面找突破口——比如分析IL,Metadata…”


 回复 引用 查看   
#50楼 2010-04-29 10:53 丛零      
“公共语言运行库通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统中只有一个。

例如,如果将同一字符串分配给几个变量,运行库就会从拘留池中检索对该字符串的相同引用,并将它分配给各个变量。

Intern 方法使用拘留池来搜索与 str 值相等的字符串。如果存在这样的字符串,则返回拘留池中它的引用。如果不存在,则向拘留池添加对 str 的引用,然后返回该引用。”

——引自MSDN

 回复 引用 查看   
#51楼 2010-11-09 09:34 chinaagan      
看了蒋老师的文章,明白了~感谢。

 回复 引用 查看   
#52楼 2010-11-10 15:07 DylanWind      
有收获,不错 !
 回复 引用 查看   
#53楼 2010-11-26 14:58 小鹤儿      
更好研究内存的时候发现的这篇文章,太好了……
 回复 引用 查看   
#54楼 2011-03-29 11:10 默含      
感谢楼主
 回复 引用 查看   
#55楼 2011-06-08 13:00 苏俊轩      
感谢楼主,豁然开朗
 回复 引用 查看   
#56楼 2011-08-11 14:40 肖敏      
引用这个Hash table的Key对应的相应的string本身
HandleTable的Key应该是string的HashCode,如果存本身的话,那不是又要重复保存一次字符串?

 回复 引用 查看   
#57楼 2011-09-15 09:17 Lee's Blog      
Equals实现了Intern吗,没有的话,在实际项目中如何运用呢?
发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 663728 M4zV+W47Fh8=