怎么理解“字符串内存大小可变”?

首页

2014-06-30

cover

这几天香港天气奇闷,让人身体不适,虽然无痛无病睡得多也在锻炼,但周五突然毫无征兆地头晕,感觉都快屎掉了,休息了整个周末才恢复。再加上最近事情略多,更新就比较少了,希望接下来能恢复起来,现在趁午休赶快说几句。

话说前几天有人在知乎上问了一个问题,里面提到为什么“C#中字符串可以使用可变大小的内存”。这个说法比较奇怪,于是我就看了他的一些补充信息,大概明白是什么意思了。

简单来说,这里说字符串内存大小“可变”有歧义,不是说一个字符串对象的内存占用可以一会儿变大一会儿变小动态调整,而是说不同字符串对象的内存占用可能不同,所以应该说“不固定”更合适。随脑一想,其实除了字符串和数组以外,好像其他相同类型的实例都是一样大的。至于说为什么,就理解为这是需求就好了,不这么做的话字符串和数组就没可用性了,所以运行时给它们开特例了。

在.NET中String类里只有两个字段——甚至说只有一个也行,即字符串长度,剩下的便是用来存储字符串内容的一块区域了,第二个char类型字段不过是用来标记内存地址。整个String类型的内存布局与非托管对象直接对应。具体可以参考Reference Source,本次封面亦是其定义。

Java中的字符串实现略有不同——严格说来是OpenJDK。在OpenJDK中,字符串内部都包含的是一个char数组,而同一个数组可以被不同的字符串对象使用,但用法在6u22前后有所改变。

在6u22之前,不同内容的字符串可以共享一个char数组。例如,从一个长的字符串对象调用substring得到的短字符串,它内部便使用了相同的char数组。可见,每个String对象内部会记录了首字符位于char数组的位置,以及这个字符串的长度。而在6u22以后,只有内容完全相同的字符串,才可能共享内部的char数组,这意味着String对象内部可以把首字符位置和长度都节省下来了,可惜在Java中String对象的length是一个只读字段而不是方法,因此必须额外保存一份了。

简单地说,6u22以后,OpenJDK的字符串就接近.NET的实现了。说是“接近”,是因为.NET中即便内容完全一致,但两个不同的字符串对象使用的内存区域也肯定不同。至于哪种做法好,见仁见智吧。OpenJDK原有的实现方式对于substring等操作的效率较高,但容易造成内存泄露——但毕竟只要人为注意,这种泄露是可以修补的。因此,像在经典的《算法(第四版)》中,Sedgewick教授便认为新的实现方式其实并不妥当,甚至可以认为是一个bug。

点击“阅读原文”可以访问知乎上的问题,其中还有关于为什么要提供不同范围的“数字类型”的讨论,不妨一读。

阅读原文