Почему хранение длинной строки вызывает ошибку OOM, а разбиение ее на список коротких строк-нет?

у меня была Java-программа, которая использовала StringBuilder чтобы построить строку из входного потока, и в конечном итоге это вызвало ошибку из памяти, когда строка стала слишком длинной. Я попытался разбить его на более короткие строки и сохранить их в ArrayList и это позволило избежать OOM, хотя я пытался сохранить тот же объем данных. Почему так?

Я подозреваю, что с одной очень длинной строкой компьютер должен найти для нее одно смежное место в памяти, но с ArrayList он может использовать несколько небольших мест в памяти. Я знаю, что память может быть сложной в Java, поэтому этот вопрос может не иметь прямого ответа, но, надеюсь, кто-то может поставить меня на правильный путь. Спасибо!

2 ответов


по сути, вы правы.

A StringBuilder (точнее, AbstractStringBuilder) использует char[] для хранения строкового представления (хотя обычно a 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, исключение создается, потому что недостаточно памяти для массива size newLength.

обратите внимание также на сообщение, данное с ошибкой: "Java heap space". Это означает, что объект (массив в данном случае) не может быть выделено в куче Java. ( Edit: есть еще одна возможная причина этой ошибки, см. 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

и тогда бросается ум.

в то время как первый программа не могла выделить больше, чем 7 * Math.pow(10, 8) ячейки массива, это подводит итог по крайней мере 8 * Math.pow(10, 8).

обратите внимание, что размер кучи может быть изменен с параметрами инициализации VM, поэтому размер, который будет бросать OOME, не является постоянным между системами.


было бы полезно, если бы вы разместили трассировку стека, если она доступна. Но есть один очень вероятная причина OutOfMemoryError что вы наблюдали.

(хотя до сих пор этот ответ может быть лишь "обоснованное предположение". Никто не может точно определить the причина без изучения условий, при которых произошла ошибка в вашей системе)

при объединении строк с помощью 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 может быть заброшенный. (Строго говоря, это не обязательно должно быть действительно "память" есть. Это просто проверка переполнения с учетом максимального размера, который может иметь массив...).

(Edit: Также посмотрите на ответ user1803551: это не обязательно должно быть место, где произошла ваша ошибка! Ваш действительно может исходить от Arrays класс, а точнее изнутри JVM)

когда внимательно изучив код, вы заметите, что размер массива равен два раза each time когда своя емкость расширена. Это важно: если это только гарантирует, что новый блок данных может быть добавлен, то добавление n символы (или другие строки с фиксированной длиной) в StringBuilder будет иметь время работы O (n2). Когда размер увеличивается с постоянным коэффициентом(здесь, 2), то время работы составляет только O (n).

однако, это удвоение размера может привести к OutOfMemoryError даже если фактический размер результирующей строки все еще намного меньше предела.