Глубокий график приводит к переполнению стека: нерекурсивные варианты сериализации? - PullRequest
6 голосов
/ 16 сентября 2011

Мы получаем StackOverflowErrors из библиотеки сериализации Java.Проблема в том, что реализация сериализации по умолчанию является рекурсивной, глубина которой ограничена только самым длинным путем через сеть ссылок.

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

Я погуглил эту тему и обнаружил, что только многие люди горько жалуютсято же самое, но большинство этих жалоб было много лет назад.Ситуация улучшилась?Если нет, и мы пишем обобщенную реализацию, есть ли у вас какие-либо советы?Мы предполагаем, что есть некоторая причина (пока не очевидная для нас), почему никто не сломал этот орех.Теоретически, делать это «правильно» звучит так, как будто это возможно.

Ответы [ 4 ]

2 голосов
/ 16 сентября 2011

У меня была эта проблема некоторое время назад.Для богато связанных классов, даже если вы можете завершить сериализацию без переполнения стека, сериализация выполняется очень медленно.Когда мы решили эту проблему, у нас было несколько классов, поэтому мы просто создали наш собственный формат сериализации, который упаковал данные в набор целочисленных идентификаторов объектов, с целочисленными идентификаторами полей для каждого поля и описал их соединения через серию идентификаторов объектов., идентификатор поля, сопоставления других идентификаторов объектов.Этот нестандартный подход был очень быстрым и чрезвычайно легким для памяти, но на самом деле работает, только если у вас есть небольшой набор классов, которые вы хотите сериализовать.

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

В принципе, вы уже столкнулись с проблемой, вам всегда будет нужна глубина стека, равная максимальной высоте дерева поиска в глубину, и поэтому каждый раз, когда ваш график окажется глубже, вы получитепереполнение стека.По сути, это рекурсивная проблема, поэтому вам нужно либо использовать рекурсию, либо фальшивую рекурсию, перемещая выделения стека в объект Stack, который вы помещаете в кучу.Я хотел бы взглянуть на реализацию OpenJDK:

http://hg.openjdk.java.net/jdk6/jdk6-gate/jdk/file/tip/src/share/classes/java/io/ObjectOutputStream.java

У вас уже есть DebugTraceInfoStack, я бы создал второе поле стека для текущего объекта, который вы пишете, и изменил бы метод writeObject0чтобы поместить объект в стек, что-то вроде этого:

stack.push(obj);
while(!stack.empty()) {
    obj = stack.pop();
    ...

Затем вы просто меняете все вызовы на writeObject0 (x);to stack.push (x) ;.Простое стандартное преобразование между рекурсией и итерацией, за исключением того, что класс почти 2500 строк и, вероятно, есть тонны ошибок.

Если вы в конечном итоге создадите его, я бы рекомендовал представить в качестве исправления для следующей версии Java, как это было бы полезно, что-то вроде IterativeObjectOutputStream для использования в графах глубоких объектов.

1 голос
/ 16 сентября 2011

Доказательство того, что сериализация JDK 6 может обрабатывать рекурсивные графы объектов:

public static void main(String[] args) throws Exception {
    Foo foo = new Foo("bob");
    foo.setBar(new Bar("fred", foo));
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream out = new ObjectOutputStream(baos);
    out.writeObject(foo);
    out.close();
    ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
    Object o = in.readObject();
    System.out.println(o);
}

static class Foo implements Serializable {
    String name;
    Bar bar;

    Foo(String name) {
        this.name = name;
    }

    void setBar(Bar bar) {
        this.bar = bar;
    }

    @Override
    public String toString() {
        return "Foo{" +
                "name='" + name + '\'' +
                ", bar=" + bar +
                '}';
    }
}

static class Bar implements Serializable {
    String name;
    Foo foo;

    Bar(String name, Foo foo) {
        this.name = name;
        this.foo = foo;
    }

    @Override
    public String toString() {
        return "Bar{" +
                "name='" + name + '\'' +
                '}';
    }
}

Выход:

Foo{name='bob', bar=Bar{name='fred'}}
0 голосов
/ 20 мая 2015

Сериализация GWT RPC в основном эквивалентна сериализации JVM, и обе используют стековую / рекурсивную технику.К сожалению, это плохо работает с разбиением работы на куски (что нужно делать, если вы работаете в браузере, то есть с GWT), поэтому вот нерекурсивный подход: https://github.com/nevella/alcina/blob/d3e37df57709620f7ad54d3d59b997e9c4c7d883/extras/rpc/client/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReader.java

По существупреобразуйте сериализацию в три этапа: * создайте экземпляры объектов * установите свойства (через связывание) * заполните коллекции

Два трюка: некоторые объекты нуждаются в свойствах при создании экземпляра (например, Date), и вам нужно заполнить коллекции последними, поскольку они могутнужны хеши своих членов.

Это допускает нерекурсивную de -сериализацию - но на самом деле нерекурсивная сериализация еще проще (до тех пор, пока нет специального writeReplace / readResolve), только внутри writeObject поддерживают две очередиобъектов-не-сериализованных, свойства-не-сериализованных-для-текущего-объекта и имеющих свойство сериализации-объекта-объекта с помощью маркера, а не рекурсивного вызова.

Есть очень простой примероб этом здесь: http://www.w3.org/2006/02/Sierra10022006.pdf

0 голосов
/ 16 сентября 2011

Кажется, я не очень хорошо прочитал вопрос.Вы, кажется, заинтересованы в сериализации свойств, которые могут содержать циклические ссылки.Если это предположение неверно и вам будет хорошо, если NOT сериализует эти объекты, содержащие циклические ссылки, пожалуйста, обратитесь к моему первоначальному ответу ниже.

Новый ответ

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

На этих объектах, которые содержат циклические ссылки, вы можете сохранить transient boolean, показывающий, был ли объект уже сериализован.Затем необходимо переопределить стандартное поведение сериализации, но это можно сделать всего несколькими строками.

public void writeExternal(ObjectOutput out) {
    if(!out.serialized) {
        out.serializeMethod();
    }
    out.serialized = true;
}

Исходный ответ

Взгляните на ключевое слово transient

Я полагаю, что большинство библиотек сериализации будут учитывать ключевое слово transient.Если элемент transient, он должен быть исключен из сериализации.

class Something {
    private Dog dog; // I will be serialized upon serialization.
    private transient SomethingElse somethingElse; // I will not be serialized upon serialization.
}

class SomethingElse {
    private Cat cat; // I will be serialized upon serialization.
    private transient Something something; // I will not be serialized upon serialization.
}

Если у вас есть рекурсивные элементы, аналогичные описанному выше сценарию, вы хотели бы отметить один или другой (или оба) какtransient чтобы не было переполнения.

...