对 String 的几个错误认识
昨天调试一段程序发现内存始终释放不掉,最后终于发现是对String 的错误使用造成,这促使我今天又仔细研究了一下String类型,不研究不知道,一研究发现我过去对String 的很多认识都是错误的,感觉这种错误认识还比较有典型性,于是写下此文和大家一起探讨。
1. String 类型变量追加,或修改后的新String对象是驻留(Interned)的。
如下面代码
string s2 = s1 + "e";
我过去想当然的认为s2 是驻留的,但实际上并非如此,用 string.IsInterned 方法检测s2是非驻留的。后来研究发现只有常量字符串才会默认驻留,其他的字符串变量哪怕是采用 new string 构造出来的,默认都非驻留,除非用string.Intern 强行驻留。后面我将提到驻留对内存的影响,微软之所以不让所有的字符串都驻留,我认为还是处于内存方面的考虑。
2. String 变量不再引用后CLR会通过GC自动释放其内存。
s1 = null;
上面代码,我想当然的认为s1 = null 后已经不再对 "abcd" 这个字符串引用,如果没有其他引用指向这个字符串,GC会释放"abcd"这块内存。实际结果却是否定的。因为s1 被赋予了一个常量,导致 "abcd"这个字符串是驻留的,驻留的字符串在进程结束之前无法被自动释放。更糟糕的是,我昨天调试的那段程序里面大量的字符串变量被采用 string.Intern 强制驻留,这导致我把所有的托管对象都释放了依然无法释放那部分大概30多M的内存。
遗憾的是微软的MSDN中文版中string.Intern 的帮助信息里面竟然漏掉了性能考谅(Performance consideration) 这一节,我估计大多数中国程序员包括我在内如果有中文的帮助是懒得去看英文的。很遗憾微软中文的帮助不知道为什么把最重要的部分给漏了。下面是英文帮助中Performance consideration 一节。
Performance Considerations
If you are trying to reduce the total amount of memory your application allocates, keep in mind that interning a string has two unwanted side effects. First, the memory allocated for interned String objects is not likely be released until the common language runtime (CLR) terminates. The reason is that the CLR's reference to the interned String object can persist after your application, or even your application domain, terminates. Second, to intern a string, you must first create the string. The memory used by the String object must still be allocated, even though the memory will eventually be garbage collected.
The .NET Framework version 2.0 introduces the CompilationRelaxations..::.NoStringInterning enumeration member. The NoStringInterning member marks an assembly as not requiring string-literal interning. You can apply NoStringInterning to an assembly using the CompilationRelaxationsAttribute attribute. Also, when you use the Native Image Generator (Ngen.exe) to compile an assembly in advance of run time, strings are not interned across modules.
看了英文的帮助就知道Intern 后的字符串是无法释放的了。
3. 两个String如果引用不同只能用Equal 比较。
我一直想当然的认为 两个String 类型如果用 == 操作符比较,将比较其引用。所以如果两个String引用不同,则只能使用Equal 来比较它们是否相等。
比如下面语句
string s3 = new StringBuilder().Append("My").Append("Test").ToString();
如下方法比较其引用
Console.WriteLine((object)s3 == (object)s2);
得到结果为 false,即s2, s3指向不同引用。
那么我想当然的认为 Console.WriteLine(s3 == s2); 的结果也是false,因为string 是引用类型,用==操作符比较引用类型变量,如果两个变量的引用不同,即便值相同,也会返回false. 然而运行的结果让我大跌眼镜。返回的值是true.
于是在网上狂搜,最后终于找到了原因。
String 的等号操作符的处理是特殊的,其源码如下
=== Equality operator on string type (C#) ===
.method public hidebysig specialname static bool
op_Equality(string a, string b) cil managed
{
.maxstack 8
L_0000: ldarg.0
L_0001: ldarg.1
L_0002: call bool System.String::Equals(
string, string)
L_0007: ret
}
从这段源码中我们看到.net 在字符串等号操作符中调用了 System.String::Equals 这个静态方法来比较。这个静态方法的代码如下。
// Determines whether two Strings match.
public static bool Equals(String a, String b) {
if ((Object)a==(Object)b) {
return true;
}
if ((Object)a==null || (Object)b==null) {
return false;
}
return EqualsHelper(a, b);
}
从这个代码我们可以看出两个string 类型在进行==操作符比较时先比较引用是否相等,如果不等会调用EqualsHelper比较值是否相等。这也就是我们看到用==操作符比较两个引用不同但值相同的string时得到true的原因。
一点建议
从时间角度考虑性能,如果字符串是驻留的,那么用==操作符比较起来,在被比较的两个字符串相等的情况下将会非常快。但从空间效率考虑,
如果对所有字符串都驻留,势必导致大量内存无法被释放。折中一下,可以在构造字符串后进行如下操作。这样构造出来的字符串如果
已经驻留,则使用驻留后的字符串引用,否则使用原来引用,这样除了可以提高比较的效率还可以减少内存的开销,因为该字符串之前已经被驻留过了,
我们没有必要再重新申请其它的内存来存储相同的字符串。 当然调用TryIntern本身会有一些性能损失,所以还要视具体情况使用,如果该字符串构造出来后
被频繁用于比较,则在第一次构造时使用TryIntern损失一些性能是值得的,否则就不值得,建议直接使用构造出来的字符串。
string s2 = new StringBuilder().Append("My").Append("Test").ToString();
s2 = TryIntern(s2);
{
string internStr = string.IsInterned(str);
return internStr == null? str: internStr;
}