为什么存储长字符串会导致 OOM 错误,但将其分解为短字符串列表却不会?

Why does storing a long string cause an OOM error but a breaking it into a list of short strings does not?

我有一个 Java 程序,它使用 StringBuilder 从输入流构建字符串,最终当字符串太长时导致内存不足错误。我尝试将它分解成更短的字符串并将它们存储在 ArrayList 中,这避免了 OOM,即使我试图存储相同数量的数据。这是为什么?

我怀疑对于一个非常长的字符串,计算机必须在内存中为它找到一个连续的位置,但是对于 ArrayList 它可以在内存中使用多个较小的位置。我知道 Java 中的记忆可能很棘手,所以这个问题可能没有直接的答案,但希望有人可以让我走上正轨。谢谢!

基本上,你是对的。

A 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 heap space"。这意味着无法在 Java 堆中分配对象(在本例中为数组)。 (编辑: 此错误还有另一个可能的原因,请参阅 )。

2.5.3. Heap

The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.

... The memory for the heap does not need to be contiguous.

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the heap, as well as, if the heap can be dynamically expanded or contracted, control over the maximum and minimum heap size.

The following exceptional condition is associated with the heap:

  • If a computation requires more heap than can be made available by the automatic storage management system, the Java Virtual Machine throws an OutOfMemoryError.

将数组分成总大小相同的较小数组可以避免 OOME,因为每个数组都可以单独存储在较小的连续区域中。当然,您 "pay" 为此必须从每个数组指向下一个数组。

将上面的代码与这个代码进行比较:

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 的大小在系统之间不是恒定的。

如果您发布了堆栈跟踪(如果有的话)可能会有所帮助。但是有一个 非常 可能导致您观察到 OutOfMemoryError 的原因。

(尽管直到现在,这个答案可能只是一个 "educated guess"。没有人可以在不检查错误发生的条件的情况下查明 的原因发生在您的系统上)

当使用 StringBuilder 连接字符串时,StringBuilder 将在内部维护一个 char[] 数组,其中包含要构造的字符串的字符。

追加字符串序列时,char[] 数组的大小可能需要在一段时间后增加。这最终在 AbstractStringBuilder 基础 class:

中完成
/**
 * This method has the same contract as ensureCapacity, but is
 * never synchronized.
 */
private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    if (minimumCapacity - value.length > 0)
        expandCapacity(minimumCapacity);
}

/**
 * This implements the expansion semantics of ensureCapacity with no
 * size check or synchronization.
 */
void expandCapacity(int minimumCapacity) {
    int newCapacity = value.length * 2 + 2;
    if (newCapacity - minimumCapacity < 0)
        newCapacity = minimumCapacity;
    if (newCapacity < 0) {
        if (minimumCapacity < 0) // overflow
            throw new OutOfMemoryError();
        newCapacity = Integer.MAX_VALUE;
    }
    value = Arrays.copyOf(value, newCapacity);
}

每当字符串生成器注意到新数据不适合当前分配的数组时调用它。

这显然是一个可能会抛出 OutOfMemoryError 的地方。 (严格来说,它不一定必须 really "out of memory" 那里。鉴于数组可以具有的最大大小,它只是检查溢出...... ).

(编辑:另请查看 :这不一定是您的错误来源!您的错误可能确实来自 Arrays class,或者更确切地说,来自 JVM 内部)

仔细检查代码,您会发现每次扩容时,数组的大小都会翻倍。这很关键:如果只确保可以追加新数据块,那么将 n 个字符(或其他固定长度的字符串)追加到 StringBuilder 将有 运行 时间O(n²)。当大小以常数因子(此处为 2)增加时,运行 时间仅为 O(n)。

但是,即使结果字符串的实际大小仍然远小于限制,这种大小的加倍可能会导致 OutOfMemoryError