我有一个使用的Java程序 StringBuilder
从输入流构建一个字符串,并最终在字符串太长时导致内存不足错误。我试着把它分成更短的字符串并将它们存储在一个字符串中 ArrayList
即使我试图存储相同数量的数据,这也避免了OOM。为什么是这样?
我的怀疑是,有一个非常长的字符串,计算机必须在内存中找到一个连续的位置,但是 ArrayList
它可以在内存中使用多个较小的位置。我知道Java中的内存可能很棘手,所以这个问题可能没有直截了当的答案,但希望有人可以让我走上正轨。谢谢!
基本上,你是对的。
一个 StringBuilder
(更确切地说, AbstractStringBuilder
)用一个 char[]
存储字符串表示(尽管通常是 String
不是一个 char[]
)。虽然Java有 不保证 一个数组确实存储在连续的内存中,它很可能是。因此,每当将字符串附加到底层数组时,就会分配一个新数组,如果它太大,则为 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)
当第3332行 char[] copy = new char[newLength];
在里面到达 Arrays.copyOf
,抛出异常,因为没有足够的内存用于大小数组 newLength
。
还要注意带有错误的消息:“Java堆空间”。这意味着无法在Java堆中分配对象(在本例中为数组)。 (编辑: 还有另一个可能的原因导致此错误,请参阅 Marco13的回答)。
2.5.3。堆
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的大小在系统之间不是恒定的。
如果您已发布堆栈跟踪(如果可用),则可能会有所帮助。但有一个 非常 可能的原因 OutOfMemoryError
你观察到的
(虽然到目前为止,这个答案可能只是一个“有根据的猜测”。没人能指出 该 没有检查系统上发生错误的条件的原因)
使用a连接字符串时 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
可能会被抛出。 (严格来说,不一定非必须如此 真 那里“记忆犹新”。鉴于数组可以拥有的最大大小,它只是检查溢出...)。
(编辑:还看看 用户1803551回答 :这不一定是您的错误来自的地方!你的确可能来自 Arrays
类,或者更确切地说来自JVM内部)
仔细检查代码时,您会注意到数组的大小是 翻倍 每当它的容量扩大时。这是至关重要的:如果它只能确保可以追加新数据块,那么附加 n
字符(或其他固定长度的字符串)到 StringBuilder
运行时间为O(n²)。当使用常数因子(此处为2)增加大小时,则运行时间仅为O(n)。
然而,这种尺寸加倍可能会导致 OutOfMemoryError
即使结果字符串的实际大小仍然远小于限制。