字符串 vs 字符数组

46

2
请您能否同时添加一下来源的引用? - allprog
3
把这些信息添加到问题中而不是使用模糊的“某个地方”会更好。 :) - allprog
为了内存性能 - Asif Bhutto
@allprog..我会记住的 :) - codingenious
2
http://www.ibm.com/developerworks/library/j-codetoheap/ - Paul Samsotha
显示剩余6条评论
4个回答

38

这个图表涉及到JDK 6- 32位。

JDK 6

在Java 7之前的世界中,字符串被实现为指向char[]数组区域的指针:

// "8 (4)" reads "8 bytes for x64, 4 bytes for x32"

class String{      //8 (4) house keeping + 8 (4) class pointer
    char[] buf;    //12 (8) bytes + 2 bytes per char -> 24 (16) aligned
    int offset;    //4 bytes                     -> three int
    int length;    //4 bytes                     -> fields align to
    int hash;      //4 bytes                     -> 16 (12) bytes
}

所以我数了一下:

36 bytes per new String("a") for JDK 6 x32  <-- the overhead from the article
56 bytes per new String("a") for JDK 6 x64.

JDK 7

仅作比较,在JDK 7+中,String是一个仅包含char[]缓冲区和一个hash字段的类。

class String{      //8 (4) + 8 (4) bytes             -> 16 (8)  aligned
    char[] buf;    //12 (8) bytes + 2 bytes per char -> 24 (16) aligned
    int hash;      //4 bytes                         -> 8  (4)  aligned
}

所以它是:

28 bytes per String for JDK 7 x32 
48 bytes per String for JDK 7 x64.

更新

对于比率为3.75:1的情况,请参见下面@Andrey的解释。随着字符串长度的增加,这个比例会降至1。

有用的链接:


480/128 = 3.75MyString 的比率,而对于单个字符的字符串则为 368/16 = 23。两个字段被删除后,数字只是稍微好一些。 - Marko Topolnik
1
@Darkhogg 自Java 7更新6以来,它已经死了。 - Marko Topolnik
1
@Darkhogg 在邮件列表中有一些内容;问题是它造成的损害比好处还要多。 - Marko Topolnik
1
@Darkhogg 是的,很不幸,它会影响某些用例。另一方面,它更透明、可预测,并且对于小字符串来说更节省空间,这意味着对于 Java 程序中使用的 99% 的所有字符串而言都是如此。净效应可能是堆使用量较少。 - Marko Topolnik
1
这个变更的原因在http://mail.openjdk.java.net/pipermail/core-libs-dev/2012-May/010257.html中简要描述。 - meriton
显示剩余9条评论

9
在JVM中,字符变量存储于单个16位内存分配中,对该Java变量的更改会覆盖同一内存位置。这使得创建或更新字符变量非常快且占用内存少,但相比于字符串中使用的静态分配,增加了JVM的开销。
JVM将Java字符串存储在可变大小的内存空间中(本质上是一个数组),该空间与在创建String对象时或首次赋值时字符串的大小完全相同(加1表示字符串终止字符)。因此,具有初始值“HELP!”的对象将被分配96位存储空间(6个字符,每个字符为16位)。这个值被认为是不可变的,允许JVM内联引用该变量,使得静态字符串分配非常快,非常紧凑,并且从JVM的角度来看非常高效。
参考:链接

1
我并不认为JVM需要终止字符。 - ratchet freak
请注意,如果您有空终止符,您可以轻松地在JVM的底层使用一些C库函数来操作字符串。至少,这是Python实现带有字符串长度字段和空终止符的字符串的一个原因。Java可能也是同样的原因。通常情况下,有时具有一些冗余是很方便的。 - Bakuriu
1
这不是一个很好的参考。char[] 不存储零终止符。Python则完全不同,它更加面向C语言。 - Marko Topolnik
@MarkoTopolnik 可能是当你分配一个 char[n] 时,JVM 会为 null 终止符分配一个额外的数组空间,但这是一个实现细节。 - ratchet freak

3
我会尝试解释源文章中提到的数字。
文章描述了对象元数据,通常包括:类、标志和锁。
类和锁存储在对象头中,在32位VM上占用8个字节。但我没有找到任何关于JVM实现中将标志信息存储在对象头中的信息。可能是这些信息存储在外部某处(例如由垃圾收集器计算对对象的引用等)。
因此,假设文章谈论的是使用12个字节的内存存储有关对象的元信息的某个x32 AbstractJVM。
然后对于char[],我们有:
- 12字节的元信息(在x32 JDK 6上为8字节,在x64 JDK上为16字节) - 4字节的数组大小 - 每个存储的字符需要2字节 - 如果字符数为奇数,则需要2字节的对齐(在x64 JDK上:2 *(4-(length + 2)%4)
对于java.lang.String,我们有:
- 12字节的元信息(在x32 JDK6上为8字节,在x64 JDK6上为16字节) - 16字节的字符串字段(对于JDK6而言,对于JDK7而言为8字节) - 如上所述,存储char[]所需的内存
因此,让我们计算将"MyString"作为String对象存储所需的内存量:
12 + 16 + (12 + 4 + 2 * "MyString".length + 2 * ("MyString".length % 2)) = 60 bytes.

从另一方面来看,我们知道仅存储数据(不包括数据类型、长度或其他任何信息)需要:

2 * "MyString".length = 16 bytes

开销是60 / 16 = 3.75

同样地,对于单个字符数组,我们得到“最大开销”:

12 + 16 + (12 + 4 + 2 * "a".length + 2 * ("a".length % 2)) = 48 bytes
2 * "a".length = 2 bytes
48 / 2 = 24

根据文章作者的逻辑,当我们存储空字符串时,无限值的最大开销最终被实现。 :)

1

我曾从stackoverflow的旧答案中了解到,无法获得它。在Oracle的JDK中,字符串具有四个实例级字段:

A character array
An integral offset
An integral character count
An integral hash value

那意味着每个字符串都会引入一个额外的对象引用(即字符串本身)和三个整数,除了字符数组本身。(偏移量和字符计数存在是为了允许通过String#substring()方法在String实例之间共享字符数组,这是一些其他Java库实现者避免的设计选择。)除了额外的存储成本,还有一个更高级别的访问间接层,更不用说String保护其字符数组的边界检查了。
如果你只分配和使用基本的字符数组,就可以节省空间。虽然在Java中这样做并不习惯,但值得谨慎地评论来证明这种选择,最好提到从分析差异中获得的证据。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接