深度图导致堆栈溢出:非递归序列化选项



我们从Java的序列化库中获得StackOverflowErrors。问题是默认的序列化实现是递归的,其深度仅受通过引用网络的最长路径的限制。

我们意识到我们可以覆盖默认方法,但是在我们的项目中有数百个丰富连接的类,所以我们对覆盖方法不感兴趣。我们更感兴趣的是是否存在一种非递归的通用解决方案(或者至少将递归从堆栈移动到堆)。

我在谷歌上搜索了这个话题,发现只有很多人在痛苦地抱怨同样的事情,但大多数抱怨都是很多年前的事了。情况好转了吗?如果没有,我们写一个通用的实现,你有什么建议吗?我们假设有一些原因(对我们来说还不明显)为什么没有人破解这个难题。从理论上讲,"正确"地做这件事听起来应该是可行的。

我之前遇到过这个问题。对于连接丰富的类,即使您能够在没有堆栈溢出的情况下完成序列化,序列化速度也很慢。当我们解决这个问题时,我们有几个类,所以我们只是创建了自己的序列化格式,将数据打包成一组整数对象id,每个字段都有整数字段id,并通过一系列对象id、字段id和其他对象id映射描述它们的连接。这种自定义方法非常快,而且占用内存非常少,但只有当你想要序列化的类很少时才有效。

一般情况要困难得多,并且对富连接类的序列化需求不是那么强烈,所以我想这就是为什么没有人解决它。

你基本上已经解决了这个问题,你总是需要一个等于深度优先搜索树的最大高度的堆栈深度,所以任何时候你的图比这个深度深,你就会得到堆栈溢出。它本质上是一个递归问题,因此您要么需要使用递归,要么需要通过将堆栈分配移动到堆上的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)的调用;stack.push (x);。简单,标准的递归和迭代之间的转换,除了类几乎有2500行,并且可能有大量的陷阱。

如果你最终要构建它,我建议把它作为一个补丁提交到下一个java版本,因为它会很有用,像IterativeObjectOutputStream这样的东西用于深度对象图。

证明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'}}

看来我没有很好地阅读问题。您似乎对序列化可能包含循环引用的属性感兴趣。如果这个假设是不正确的,并且您可以使用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,以便不会发生溢出。

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-serialization -但实际上非递归序列化甚至更简单(只要没有自定义writeReplace/readResolve),只需在writeObject中维护两个objectnot -serialized, propertiesnot - serializedofcurrent -object的队列,并将serialize-object-property用标记推入堆栈,而不是进行递归调用。

这里有一个非常基本的例子:http://www.w3.org/2006/02/Sierra10022006.pdf

相关内容

  • 没有找到相关文章

最新更新