基本上,你是对的。
StringBuilder(更准确地说,AbstractStringBuilder)使用char[] 来存储字符串表示形式(尽管通常String 不是char[])。虽然 Java 确实 not guarantee 表明数组确实存储在连续内存中,但它很可能是。因此,每当将字符串附加到底层数组时,都会分配一个新数组,如果它太大,则会抛出 OutOfMemoryError。
确实,执行代码
StringBuilder b = new StringBuilder();
for (int i = 0; i < 7 * Math.pow(10, 8); i++)
b.append("a"); // line 11
抛出异常:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at test1.Main.main(Main.java:11)
当Arrays.copyOf 内部到达第3332 行char[] copy = new char[newLength]; 时,会抛出异常,因为没有足够的内存用于大小为newLength 的数组。
还要注意与错误一起给出的消息:“Java 堆空间”。这意味着无法在 Java 堆中分配对象(在本例中为数组)。 (编辑:此错误还有另一个可能的原因,请参阅Marco13's answer)。
2.5.3. Heap
Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的堆。堆是为所有类实例和数组分配内存的运行时数据区域。
...堆的内存不需要是连续的。
Java 虚拟机实现可以让程序员或用户控制堆的初始大小,如果堆可以动态扩展或收缩,还可以控制最大和最小堆大小。
以下异常情况与堆相关:
- 如果计算需要的堆比自动存储管理系统所能提供的更多,Java 虚拟机会抛出
OutOfMemoryError。
将数组分成总大小相同的较小数组可以避免 OOME,因为每个数组可以单独存储在较小的连续区域中。当然,您为此“付费”,必须从每个数组指向下一个数组。
将上面的代码与下面的代码进行比较:
static StringBuilder b1 = new StringBuilder();
static StringBuilder b2 = new StringBuilder();
...
static StringBuilder b10 = new StringBuilder();
public static void main(String[] args) {
for (int i = 0; i < Math.pow(10, 8); i++)
b1.append("a");
System.out.println(b1.length());
// ...
for (int i = 0; i < Math.pow(10, 8); i++)
b10.append("a");
System.out.println(b10.length());
}
输出是
100000000
100000000
100000000
100000000
100000000
100000000
100000000
100000000
然后抛出 OOME。
虽然第一个程序分配的数组单元数不能超过7 * Math.pow(10, 8),但这个总和至少为8 * Math.pow(10, 8)。
请注意,堆的大小可以通过 VM 初始化参数来更改,因此引发 OOME 的大小在系统之间不是恒定的。