再说String
String主要具有以下的两个显著的特点:
- String的恒定性:String一经创建,它所对应的字符序列就无法更改(当然我们的前提是托管的环境下)。
- String的驻留:CLR对String的创建实行驻留机制,CLR只会维护具有不同字符序列的String。换言之,在程序中使用到的具有完全相同的字符序列的String均是对应着同一个string对象,是对同一个段内存的引用。值得一提的是String的这种驻留机制不仅仅是基于某个单独的AppDomain的,而是针对整个进程的。
关于Process-wide字符串驻留机制的存在,我想我在《深入理解string和如何高效地使用string》中的Sample已经很明显的证明的这点。不过文中并没有为此提供充分的理论的基础,现在我就来谈谈为什么String的驻留是跨AppDomain的。
要明白Process-wide字符串驻留机制的原理,必须首先了解一个托管程序是如何运行的。
当我们运行一个托管程序,我们知道CLR会为此创建一个Default AppDomain,但实际上Windows为我们作的事情远不止这么简单。之所以我们说一个Application是在一个托管的环境下执行的,是指的是CLR对他进行托管。所以在这之前,对CLR的加载是必须的。我们知道.NET Framework是建立在Windows平台之上的,如果说Windows是对计算机硬件的封装的话,.NET Framework则可以看成是对Windows的封装,通过.NET Framework API封装了对传统Win32的封装。正是因为Windows是.NET Framework的基础架构,所以.NET Framework只能是利用Windows所能理解的方式进行构建。而对于一个Windows来说,所有能被加载执行的都是一个PE文件(Portable Executable file),比如exe和dll。CLR也不能免俗,他实际上是一个COM Server的形式实现在一个叫做MSCorWks.Dll中,该Dll存在于.NET Framework对应的目录中。
当程序开始运行的时候,有一个称为SystemDomain的AppDomain被创建,SystemDomain加载一个名为MSCorEE.Dll,该Dll就是我们经常所说的“垫片”(shim)。通过定义在该垫片中的一个名为CorBindToRuntimeEx的函数加载对应版本的CLR,并返回一个非托管的ICLRRuntimeHost interface。SystemDomain可以说是整个Process的枢纽,它负责创建、初始化、卸载SharedDomain和DefaultDomain。
我们知道AppDomain是一个Assembly的托管容器,Assembly在一般情况下是基于某个单独的AppDomain的,不能与另一个AppDomain共享的。但是有些公用性很强的Assembly,比如我们经常使用的一些基元类型object, int,Array,ValueType等,却希望它被一个AppDomain加载之后,能够被其他的AppDomain共享,这样可以省去很多内存空间和Assembly加载带来的性能损失。这些Assembly就是被加载到SharedDomain中,我们常用的MScorLib.dll就是被以这样的方式被加载的SharedDomain中的。Default Domain就是为具体的Application创建的AppDomain,它一般以可执行文件名命名。DefaultDomain中可以通过AppDomain.CreateAppDomain创建另一个AppDomain。所以当我们运行一个托管的Application的时候,实际上创建了3个不同AppDomain:SystemDomain,ShatedDomain和DefaultDomain,而SystemDomain和ShatedDomain基于整个进程的,能够被DefaultDomain以及被它创建AppDomain共享的。
有了上面的基础,我想我们就不难理解String的驻留机制的。String的驻留机制实际上是在SystemDomain中进行的。当CLR被加载之后,会在SystemDomain对应的managed heap中创建一个Hash table的数据结构,我们可以称这个Hashtable为Interning table,因为它是被用来保存被驻留的string的,Interning table的Key为string本身,Value为string对象的地址。
当我们的托管程序(无论对于那个AppDomain)需要一个string的时候,CLR首先在这个Hashtable根据这个string的hash code试着在Interning table中找对应的Item。如果成功找到,则直接把对应的引用返回,否则就在SystemDomain对应的managed heap中创建该string,并加入到Interning table中,并把引用返回。所以我们说字符串的驻留是基于整个进程的,是可以跨AppDomain共享的,就是这个道理。