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

时间:2017-07-31 00:28:59

标签: java string out-of-memory heap-memory

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

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

2 个答案:

答案 0 :(得分:6)

基本上,你是对的。

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)

char[] copy = new char[newLength];内到达第3332行Arrays.copyOf时,抛出异常,因为没有足够的内存来容纳大小为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的大小在系统之间不是恒定的。

答案 1 :(得分:3)

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

(虽然到现在为止,这个答案可能只是一个“有根据的猜测”。没有人可以在不检查系统错误发生的条件下查明 原因)

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

当附加一系列字符串时,此char[]数组的大小可能必须在一段时间后增加。这最终在AbstractStringBuilder基类中完成:

/**
 * 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的地方。 (严格地说,它不一定必须真的“内存不足”。它只是根据数组可以拥有的最大大小来检查溢出...)。 / p>

(编辑:另请查看answer by user1803551:这不一定是您的错误来自的地方!您的确可能来自Arrays类,或者而是来自JVM内部)

仔细检查代码时,每次扩展容量时,您会注意到数组的大小加倍。这是至关重要的:如果它只能确保可以附加新的数据块,那么将n个字符(或其他固定长度的字符串)附加到StringBuilder的运行时间为O(n²) )。当使用常数因子(此处为2)增加大小时,则运行时间仅为O(n)。

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