Java 序列化时是否保留了内部字符串?

Java 序列化时是否保留了内部字符串?,java,serializable,string-interning,Java,Serializable,String Interning,如果我有一个包含许多重复字符串的大型对象图,那么在序列化字符串之前先对字符串进行内接有什么好处吗?这会减少传输的数据量吗?字符串是否在接收端共享指针 我的猜测是,字符串在发送之前将被消除重复,从而减少数据的大小,并且它们都将由接收端的同一对象表示,但它们实际上不会被保存在接收端。(这意味着在每个序列化“事务”上都会创建一个新的字符串实例)很容易测试: import java.io.*; class Foo implements Serializable { private String

如果我有一个包含许多重复字符串的大型对象图,那么在序列化字符串之前先对字符串进行内接有什么好处吗?这会减少传输的数据量吗?字符串是否在接收端共享指针


我的猜测是,字符串在发送之前将被消除重复,从而减少数据的大小,并且它们都将由接收端的同一对象表示,但它们实际上不会被保存在接收端。(这意味着在每个序列化“事务”上都会创建一个新的字符串实例)

很容易测试:

import java.io.*;

class Foo implements Serializable {
    private String x;
    private String y;

    public Foo(String x, String y) {
        this.x = x;
        this.y = y;
    }
}

public class Test {
    public static void main(String[] args) throws IOException {
        String x = new StringBuilder("hello").append(" world").toString();
        String y = "hello world";

        showSerializedSize(new Foo(x, y));
        showSerializedSize(new Foo(x, x));
    }

    private static void showSerializedSize(Foo foo) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(foo);
        oos.close();
        System.out.println(baos.size());
    }
}
在我的机器上的结果:

86
77
因此,看起来重复数据消除不会自动发生


不过我不会使用
String.intern()
本身,因为您可能不希望所有这些字符串都出现在普通的intern池中-但您可以始终使用
哈希集创建“临时”intern池。

ObjectOutputStream跟踪对象图(直到重置),一个对象只写入一次,即使它是通过多次引用来实现的。通过实习减少对象肯定会减少字节数


在接收端,将重新创建相同的对象图,因此发送端的一个字符串实例将成为接收端的一个字符串实例。

您可以使用
ObjectOutputStream
的此增强功能,它实现了
字符串的重复数据消除。输出应与原始版本兼容(未测试),因此不需要特殊的
ObjectInputStream

请注意,使用的不是
String.intern()
,而是一个私有的、临时的内部
映射
,因此您的永久空间不会被淹没

public class StringPooledObjectOutputStream extends ObjectOutputStream {
    private Map<String, String> stringPool = new HashMap<String, String>();
    public StringPooledObjectOutputStream(OutputStream out) throws IOException {
        super(out);
        enableReplaceObject(true);
    }

    @Override
    protected Object replaceObject(Object obj) throws IOException {
        if( !(obj instanceof String) )
            return super.replaceObject(obj);

        String str = (String)obj;

        String replacedStr = stringPool.get(str);
        if( replacedStr == null ){
            replacedStr = (String)super.replaceObject(str);
            stringPool.put(replacedStr, replacedStr);
        }
        return replacedStr;
    }
}
公共类StringPooledObjectOutputStream扩展了ObjectOutputStream{
私有映射stringPool=newHashMap();
public StringPooledObjectOutputStream(OutputStream out)抛出IOException{
超级(出局);
enableReplaceObject(true);
}
@凌驾
受保护对象替换对象(对象obj)引发IOException{
如果(!(字符串的obj实例))
返回super.replaceObject(obj);
字符串str=(字符串)obj;
String replacedStr=stringPool.get(str);
if(replacedStr==null){
replacedStr=(字符串)super.replaceObject(str);
stringPool.put(replacedStr,replacedStr);
}
返回替换器;
}
}

在序列化之前,插入字符串似乎没有任何好处。至少这不会改变序列化的任何内容。它可能有助于减少应用程序的内存

在接收方,将调用
ObjectOutPutStream
的最低级别
readUTF()
或其等效项,为每次调用分配新字符串。如果您的类是可外部化的,您可以执行
readUTF().intern()
以节省接收方的内存。我自己也使用过这种方法,并将客户端应用程序的内存使用量减少了50%以上

但是请注意,如果有许多唯一字符串,则
intern()
可能会因为使用PermGen而导致内存不足问题。见:


我只保留了小于10个字符的字符串,并且没有遇到任何问题。

或者Guava's and,如果您想重用hashmap,但担心其增长/泄漏,则包括可选的WeakHashMap支持。感谢简单的测试用例。出于某种原因,我没有想到我可以在一个内存空间中如此轻松地测试它。这似乎是一个很好的“暴力”解决方案。在我的例子中,我已经选择性地插入了已知具有少量值的字符串,因此我不需要在流级别进行重复数据消除。如果我想在牺牲一些cpu周期的情况下获得更多的压缩,这将是很好的。谢谢。@JamesScriven:我不理解“暴力”部分,因为代码使用了
ObjectOutputStream
提供的机制。还要注意,每个实例只调用一次
replaceObject
。我会称之为“温和地”:-)但如果您的意思是重复数据消除本身不适合您的情况,那么也没关系。