6

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

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

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

4

4 回答 4

2

我前段时间遇到过这个问题。对于连接丰富的类,即使你能够在没有堆栈溢出的情况下完成序列化,序列化也很慢。当我们解决这个问题时,我们有几个类,所以我们只是创建了自己的序列化格式,将数据打包成一组整数对象 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,我将为您正在编写的当前对象创建第二个 Stack 字段,并更改 writeObject0 方法以将对象推送到堆栈上,如下所示:

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

然后您只需将所有调用更改为 writeObject0(x); 到 stack.push(x);. 递归和迭代之间的简单、标准转换,除了这个类几乎有 2500 行并且可能有很多陷阱。

如果你最终构建它,我建议将 at 作为下一个版本的 java 的补丁提交,因为它会很有用,比如 IterativeObjectOutputStream 用于深度对象图。

于 2011-09-16T05:57:28.670 回答
1

证明 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'}}
于 2011-09-16T02:53:24.403 回答
0

看来我没有很好地阅读这个问题。您似乎对序列化可能包含循环引用的属性感兴趣。如果这个假设不正确,并且您可以序列化这些包含循环引用的对象,请参阅下面我的原始答案。

新答案

我认为您需要跟踪哪些对象已被序列化,除非您自己做,否则我看不到这种情况发生。不过应该不会太难。

在这些包含循环引用的对象上,您可以保留一个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以便不会发生这种溢出。

于 2011-09-16T01:58:49.157 回答
0

GWT RPC 序列化基本上等同于 JVM 序列化,并且都使用堆栈/递归技术。不幸的是,这不适用于将工作切成块(如果您在浏览器中工作,即使用 GWT,则需要这样做),所以这是一种非递归方法: https ://github.com/nevella/ alcina/blob/d3e37df57709620f7ad54d3d59b997e9c4c7d883/extras/rpc/client/src/com/google/gwt/user/client/rpc/impl/ClientSerializationStreamReader.java

本质上,将序列化转换为三遍: * 实例化对象 * 设置属性(通过链接) * 填充集合

两个技巧:一些对象需要实例化属性(例如日期),并且您需要最后填充集合,因为它们可能需要其成员的哈希值。

这允许非递归反序列化-但实际上非递归序列化更简单(只要没有自定义 writeReplace/readResolve),只需在 writeObject 内维护两个对象队列 - 未序列化,属性 - 未序列化-for-current-object 并使用标记将 serialize-object-property 推送到堆栈,而不是进行递归调用。

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

于 2015-05-20T06:11:02.593 回答