Неожиданный OutOfMemoryError при использовании ObjectInputStream # readUnshared()

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

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

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

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

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

Из всех моих исследований по этой теме показывается, что 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.
    }

}

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

  • Запустите тестовую программу с помощью testWrite uncommented (и testRead not called) с высоким размером кучи, поэтому writeObject не приведет к OOM.
  • Запустите программу тестирования второй раз с testRead uncommented (и testWrite not called) с размером кучи по умолчанию.

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

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

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

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

  • Все мои исследования пока не показывают никакой связи между поведением readUnshared и тем, как были написаны объекты. Что здесь происходит?
  • Есть ли способ избежать чтения OOM при условии, что данные были написаны с помощью writeObject и no 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 ГБ). Здесь есть четыре тестовых файла, каждый из которых генерируется с различными комбинациями writeObject/writeUnshared и reset. Поведение чтения зависит только от того, как оно было написано и не зависит от readObject vs. readUnshared. Обратите внимание, что файлы данных writeObject vs writeUnshared идентичны байтам для байта, я не могу решить, удивительно это или нет.


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

ObjectStreamClass desc = readClassDesc(false);

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

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

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

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

Ответ 1

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

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