在Java中将对象序列化和压缩的性能成本

4
应用程序不断接收名为Report的对象,并将这些对象放入Disruptor以供三个不同的消费者使用。
通过Eclipse Memory Analysis,每个Report对象的保留堆大小平均为20KB。应用程序以-Xmx2048启动,表示应用程序的堆大小为2GB。
然而,每次对象数量约为10万个,这意味着所有对象的总大小大约为2GB。
要求是将所有的10万个对象加载到Disruptor中,以便消费者异步地消费数据。但如果每个对象的大小达到20KB,这是不可能实现的。
因此,我想将对象序列化为String并进行压缩:
private static byte[] toBytes(Serializable o) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(o);
    oos.close();

    return baos.toByteArray();
}

private static String compress(byte[] str) throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    GZIPOutputStream gzip = new GZIPOutputStream(out);
    gzip.write(str);
    gzip.close();
    return new String(Base64Coder.encode(out.toByteArray()));
}

在使用compress(toBytes(Report))进行压缩后,对象的大小变小了:

压缩前

Before compression

压缩后

After compression

现在对象的字符串大小约为6KB,这是一个更好的结果。

以下是我的问题:

  1. 是否有其他数据格式比字符串更小?

  2. 每次调用序列化和压缩都会创建像ByteArrayOutputStreamObjectOutputStream等对象。我不想创建太多这样的对象,因为我需要迭代100,000次。如何设计代码,使得像ByteArrayOutputStreamObjectOutputStream这样的对象只创建一次,并在每次迭代中重复使用?

  3. 消费者需要对来自Disruptor的字符串进行反序列化和解压缩。如果我有三个消费者,那么我需要三次反序列化和解压缩。有什么解决方法吗?


更新:

正如@BoristheSpider建议的那样,序列化和压缩应该在一次操作中执行:

private static byte[] compressObj(Serializable o) throws IOException {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    GZIPOutputStream zos = new GZIPOutputStream(bos);
    ObjectOutputStream ous = new ObjectOutputStream(zos);

    ous.writeObject(o);
    zos.finish();
    bos.flush();

    return bos.toByteArray();
}

这可能不是正确的方法。它会对性能产生巨大的影响。有一些专门针对这种情况的设计模式,比如享元模式。另外,为什么不直接压缩流呢?为什么要先创建byte[]然后再压缩它呢? - Boris the Spider
该应用程序接收一个自定义对象“Report”。我的做法是将“Report”序列化并压缩序列化的字符串。您建议直接压缩“Report”吗? - macemers
我建议将ByteArrayOutputStream包装在一个GZIPOutputStream中,然后再包装在ObjectOutputStream中。这样可以在一次操作中序列化和压缩。 - Boris the Spider
还有其他数据格式的大小比String更小吗” - 当然有。在创建base64编码的String之前,您拥有的字节数组要紧凑得多。为什么不保留字节数组而不是创建String?有时您可能希望解压报告(否则根本不需要存储它),到那时您将再次需要一个字节数组。 - Holger
@BoristheSpider 添加了一个更新,请参考。 - macemers
@Holger 你说得对。byte[] 更小。在我的情况下,对象大小为20k,String大小为6k,但是 byte[] 大约只有2k。 - macemers
1个回答

0

使用ObjectOutputStream和压缩比使用Disruptor昂贵得多,这使得使用它的目的毫无意义。它可能会昂贵1000倍。

除非您的设计有严重问题,否则最好限制一次排队的对象数量。拥有仅1000个20 KB对象的队列应该足以确保所有消费者都能高效工作。

顺便说一下,如果您需要持久性,我建议使用Chronicle(部分原因是我写的)。这不需要压缩或byte[]或字符串进行存储,持久化所有消息,您的队列是无界的,并且完全脱离堆。即您的100K对象将使用<<1 MB的堆。


@user838204 如果是这样的话,我会将其输出到一个Chronicle中,并在数据传输时持久化数据,这样你就不必担心数据库落后于一小时还是一天。如果你需要冗余备份,Chronicle支持TCP复制。 - Peter Lawrey
你可以使用消息代理器,例如ActiveMQ,而不是@user838204。它可以配置为持久化消息等。 - Boris the Spider
@user838204 需要考虑的一点是,Chronicle 的设计是记录所有数据,直到你在外部删除它,例如每天一次。这对于在测试中重放长序列的真实数据或复制错误非常有用。然而,它假定磁盘空间非常便宜,这在所有组织中都不是真的。 - Peter Lawrey
@user838204 正在开发一个 SharedQueue,它会在读取消息后立即消费。这将使用更少的磁盘空间,但您将没有记录。 - Peter Lawrey
感谢您的详细解释,我已经 Fork 了 Chronicle 并开始研究它。我能为 Chronicle 做出贡献吗?因为我真的想学习如何在 Java 中编写高性能应用程序。附带说一下,我的问题在这里有单独的描述:https://dev59.com/V37aa4cB1Zd3GeqPpV4W?noredirect=1#comment34876833_22859618 请看一下,看看 Chronicle 是否有所帮助? - macemers
显示剩余10条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接