Неожиданная ошибка OutOfMemoryError при использовании ObjectInputStream # readUnshared ()

Я сталкиваюсь с OOM при чтении большого количества объектов из ObjectInputStream с readUnshared. MAT указывает на свою внутреннюю таблицу дескрипторов как на виновника, как и трассировка стека OOM (в конце этого сообщения ). По общему мнению, этого не должно происходить. Более того, возникновение OOM зависит от того, как объекты были написаны ранее.

Согласно эта статья по теме, readUnshared должен решить проблему (в отличие от readObject), не создавая записи таблицы дескрипторов во время чтения (именно в этой записи я обнаружил writeUnshared и readUnshared, которых раньше не замечал).

Однако из моих собственных наблюдений видно, что readObject и readUnshared ведут себя одинаково, и то, произойдет ли OOM или нет, зависит от того, были ли объекты записаны с помощью _ 9_ после каждой записи (не имеет значения, использовалось ли writeObject против writeUnshared, как я думал ранее - я просто устал, когда впервые запустил тесты). То есть:

              writeObject   writeObject+reset   writeUnshared   writeUnshared+reset
readObject       OOM               OK               OOM                 OK
readUnshared     OOM               OK               OOM                 OK

Так что, имеет ли readUnshared какой-либо эффект на самом деле, кажется, полностью зависит от того, как был написан объект. Для меня это удивительно и неожиданно. Я потратил некоторое время на отслеживание _ 13_ путь кода, но, учитывая, что это было поздно и я устал, мне не было очевидно, почему он все еще использует пространство дескрипторов и почему будет зависеть от того, как был написан объект (однако, теперь у меня есть первоначальный подозреваемый, хотя я еще не подтвердил, как описано ниже).

Из всех моих исследований по этой теме, похоже, writeObject с readUnshared должен работать.

Вот программа, с которой я тестировал:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;


public class OOMTest {

    // This is the object we'll be reading and writing.
    static class TestObject implements Serializable {
        private static final long serialVersionUID = 1L;
    }

    static enum WriteMode {
        NORMAL,     // writeObject
        RESET,      // writeObject + reset each time
        UNSHARED,   // writeUnshared
        UNSHARED_RESET // writeUnshared + reset each time
    }

    // Write a bunch of objects.
    static void testWrite (WriteMode mode, String filename, int count) throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(filename)));
        out.reset();
        for (int n = 0; n < count; ++ n) {
            if (mode == WriteMode.NORMAL || mode == WriteMode.RESET)
                out.writeObject(new TestObject());
            if (mode == WriteMode.UNSHARED || mode == WriteMode.UNSHARED_RESET)
                out.writeUnshared(new TestObject());
            if (mode == WriteMode.RESET || mode == WriteMode.UNSHARED_RESET)
                out.reset();
            if (n % 1000 == 0)
                System.out.println(mode.toString() + ": " + n + " of " + count);
        }
        out.close();
    }

    static enum ReadMode {
        NORMAL,     // readObject
        UNSHARED    // readUnshared
    }

    // Read all the objects.
    @SuppressWarnings("unused")
    static void testRead (ReadMode mode, String filename) throws Exception {
        ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(filename)));
        int count = 0;
        while (true) {
            try {
                TestObject o;
                if (mode == ReadMode.NORMAL)
                    o = (TestObject)in.readObject();
                if (mode == ReadMode.UNSHARED)
                    o = (TestObject)in.readUnshared();
                //
                if ((++ count) % 1000 == 0)
                    System.out.println(mode + " (read): " + count);
            } catch (EOFException eof) {
                break;
            }
        }
        in.close();
    }

    // Do the test. Comment/uncomment as appropriate.
    public static void main (String[] args) throws Exception {
        /* Note: For writes to succeed, VM heap size must be increased.
        testWrite(WriteMode.NORMAL, "test-writeObject.dat", 30_000_000);
        testWrite(WriteMode.RESET, "test-writeObject-with-reset.dat", 30_000_000);
        testWrite(WriteMode.UNSHARED, "test-writeUnshared.dat", 30_000_000);
        testWrite(WriteMode.UNSHARED_RESET, "test-writeUnshared-with-reset.dat", 30_000_000);
        */
        /* Note: For read demonstration of OOM, use default heap size. */
        testRead(ReadMode.UNSHARED, "test-writeObject.dat"); // Edit this line for different tests.
    }

}

Шаги по воссозданию проблемы с этой программой:

  1. Запустите тестовую программу с testWrites без комментариев (и testRead без вызова) с высоким размером кучи, поэтому writeObject не приведет к OOM.
  2. Запустите тестовую программу второй раз с testRead без комментариев (и testWrite без вызова) с размером кучи по умолчанию.

Чтобы было ясно: я не занимаюсь записью и чтением в одном экземпляре JVM. Мои записи происходят в отдельной программе от моих чтений. Приведенная выше тестовая программа на первый взгляд может немного вводить в заблуждение из-за того, что я втиснул тесты записи и чтения в один и тот же источник.

К сожалению, реальная ситуация, в которой я нахожусь, заключается в том, что у меня есть файл, содержащий множество объектов, написанных с помощью writeObject (без reset), для регенерации которого потребуется некоторое время (порядка дней) (а также reset делает вывод файлы огромны), поэтому я бы хотел избежать этого, если это возможно. С другой стороны, в настоящее время я не могу прочитать файл с readObject, даже если пространство кучи увеличено до максимума, доступного в моей системе.

Стоит отметить, что в моей реальной ситуации мне не нужно кеширование, обеспечиваемое таблицами дескрипторов объектных потоков.

Итак, мои вопросы:

  1. Все мои исследования пока не показывают никакой связи между поведением readUnshared и тем, как были написаны объекты. Что здесь происходит?
  2. Есть ли способ избежать OOM при чтении, учитывая, что данные были записаны с writeObject, а не reset?

Я не совсем уверен, почему readUnshared не может решить проблему здесь.

Надеюсь, это понятно. Я здесь пусто, поэтому, возможно, набрал странные слова.


Из комментариев к ответу ниже:

Если вы не вызываете writeObject() в текущем экземпляре JVM, вам не следует использовать память, вызывая readUnshared().

Все мои исследования показывают одно и то же, но сбивает с толку:

  • Вот трассировка стека OOM, указывающая на readUnshared:

    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.io.ObjectInputStream$HandleTable.grow(ObjectInputStream.java:3464)
    at java.io.ObjectInputStream$HandleTable.assign(ObjectInputStream.java:3271)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1789)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350)
    at java.io.ObjectInputStream.readUnshared(ObjectInputStream.java:460)
    at OOMTest.testRead(OOMTest.java:40)
    at OOMTest.main(OOMTest.java:54)
    
  • Вот видео об этом (видео, записанное до недавнего редактирования тестовой программы, видео эквивалентно ReadMode.UNSHARED и WriteMode.NORMAL в новой тестовой программе).

  • Вот несколько файлов тестовых данных, которые содержат 30 000 000 объектов (сжатые размер составляет крошечные 360 КБ, но имейте в виду, что он увеличивается до колоссальных 2,34 ГБ). Здесь есть четыре тестовых файла, каждый из которых сгенерирован с различными комбинациями _36 _ / _ 37_ и reset. Поведение при чтении зависит только от того, как оно было написано, и не зависит от readObject vs. readUnshared. Обратите внимание, что файлы данных writeObject vs writeUnshared идентичны побайтно, я не могу решить, удивительно это или нет.


Я смотрел на ObjectInputStream код, который отсюда. Мой текущий подозреваемый - эта строка, присутствующая в 1.7 и 1.8:

ObjectStreamClass desc = readClassDesc(false);

Где этот параметр boolean равен true для общего доступа и false для обычного. Во всех других случаях флаг «без общего доступа» распространяется на другие вызовы, но в этом случае он жестко запрограммирован на false, что приводит к добавлению дескрипторов в таблицу дескрипторов при чтении описаний классов для сериализованных объектов даже при использовании readUnshared. AFAICT, это единственный случай, когда флаг unshared не передается другим методам, поэтому я сосредоточился на нем.

Это контрастирует, например, с это строка, в которой флаг отсутствия общего доступа передается в readClassDesc. (Вы можете проследить путь вызова от readUnshared до обеих этих строк, если кто-то захочет покопаться.)

Однако я еще не подтвердил, что это важно, и не объяснил, почему там жестко запрограммирован false. Это просто текущий трек, который я сейчас рассматриваю, он может оказаться бессмысленным.

Кроме того, у fwiw ObjectInputStream есть частный метод clear, который очищает таблицу дескрипторов. Я провел эксперимент, в котором я вызвал это (через отражение) после каждого чтения, но он просто сломал все, так что это недопустимо.


person Jason C    schedule 11.03.2017    source источник


Ответы (2)


Однако кажется, что если объекты были написаны с использованием writeObject(), а не writeUnshared(), то readUnshared() не уменьшает использование таблицы дескрипторов.

Это правильно. readUnshared() только уменьшает использование таблицы дескрипторов, относящееся к readObject(). Если вы находитесь в той же JVM, которая использует writeObject(), а не writeUnshared(), использование таблицы дескрипторов, относящееся к writeObject(), не уменьшается на readUnshared().

person user207421    schedule 11.03.2017
comment
Хм. Когда вы говорите в одной и той же JVM, вы имеете в виду один и тот же экземпляр? В моем случае объекты были написаны отдельной программой, я не выполняю writeObject и readUnshared в одном прогоне. В тестовой программе только один из testWrite и testRead следует раскомментировать и запускать в любой момент времени. Мой сериализованный файл данных уже существует до выполнения чтения. - person Jason C; 11.03.2017
comment
Если вы не вызываете writeObject() в текущем экземпляре JVM, вы не должны использовать память, вызывая readUnshared(). - person user207421; 11.03.2017
comment
Вот что я говорю! Этого не должно происходить. :) Тем не менее, это так. Я добавил трассировку стека к вопросу, указывающему на readUnshared, а также на видеодоказательства . - person Jason C; 11.03.2017
comment
Итак, я внес существенные изменения в вопрос. Я был утомлен, когда проводил эти тесты. Разница не в writeObject и writeUnshared, а в том, ставлю ли я reset между записями. Та же загадка (readObject vs readUnshared ведут себя одинаково и зависят только от метода записи), только разные наблюдения. - person Jason C; 12.03.2017

writeUnShared() по-прежнему пишите null в свой handlers, который будет расти по мере того, как вы напишете больше объектов. вот почему у вас OOM на readUnShared.

проверьте это: OutOfMemoryException: Утечка памяти в Java-классах ObjectOutputStream и ObjectInputStream

person D3Hunter    schedule 27.02.2018