我们从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