也许我很天真,但是我一直认为Java序列化肯定是将Java对象序列化为二进制形式的最快,最有效的方法。 毕竟Java是第7个主要发行版,所以这不是新技术,并且由于每个JDK似乎都比上一个快,因此我错误地认为序列化现在必须非常快速和高效。 我认为,由于Java序列化是二进制的,并且依赖于语言,因此它必须比XML或JSON更快,更高效。 不幸的是,我错了,如果您担心性能,建议不要使用Java序列化。
现在,请不要误会我的意思,我不是在尝试破坏Java。 Java序列化有许多要求,主要的需求是能够将任何东西(或至少任何实现Serializable
东西) Serializable
到任何其他JVM(甚至是不同的JVM版本/实现)中,甚至运行被序列化的类的不同版本(例如只要您设置了serialVersionUID
)。 最主要的是,它确实有效,而且确实很棒。 性能不是主要要求,格式是标准的并且必须向后兼容,因此优化非常困难。 而且,对于许多类型的用例,Java序列化执行得很好。
在研究三层并发基准时,我开始了进入序列化过程的旅程。 我注意到Java序列化过程中花费了大量CPU时间,因此我决定进行调查。 我从序列化具有几个字段的简单Order
对象开始。 我序列化了对象并输出了字节。 尽管Order对象只有几个字节的数据,但我并不是天真地认为它将序列化为几个字节,但我对序列化足够了解,因此至少需要写出完整的类名,因此它知道它已序列化的内容,因此可以将其读回。 因此,我期望大约50个字节。 结果超过了600个字节,那时候我意识到Java序列化并不像我想象的那么简单。
Order对象的Java序列化字节
----sr--model.Order----h#-----J--idL--customert--Lmodel/Customer;L--descriptiont--Ljava/lang/String;L--orderLinest--Ljava/util/List;L--totalCostt--Ljava/math/BigDecimal;xp--------ppsr--java.util.ArrayListx-----a----I--sizexp----w-----sr--model.OrderLine--&-1-S----I--lineNumberL--costq-~--L--descriptionq-~--L--ordert--Lmodel/Order;xp----sr--java.math.BigDecimalT--W--(O---I--scaleL--intValt--Ljava/math/BigInteger;xr--java.lang.Number-----------xp----sr--java.math.BigInteger-----;-----I--bitCountI--bitLengthI--firstNonzeroByteNumI--lowestSetBitI--signum[--magnitudet--[Bxq-~----------------------ur--[B------T----xp----xxpq-~--xq-~--
(注意“-”表示不可打印的字符)
您可能已经注意到,Java序列化不仅写出要序列化的对象的完整类名,而且还写出要序列化的类的整个类定义以及所有引用的类。 类定义可能非常大,并且似乎是主要的性能和效率问题,尤其是在编写单个对象时。 如果要写出大量相同类的对象,则类定义开销通常不是大问题。 我注意到的另一件事是,如果您的对象具有对类的引用(例如元数据对象),则Java序列化将编写整个类定义,而不仅仅是类名,因此使用Java序列化来编写元数据非常昂贵。
可外部化
通过实现Externalizable接口可以优化Java序列化。 实现此接口可以避免写出整个类定义,而只需编写类名即可。 它要求您实现readExternal
和writeExternal
方法,因此需要您进行一些工作和维护,但是比仅实现Serializable更快,更高效。
关于Externalizable结果的一个有趣注释是,对于少量对象,它的效率要高得多,但对于大量对象,实际上输出的字节数要比Serializable多。 我假设Externalizable格式对重复对象的效率稍低。
可外部化的类
public class Order implements Externalizable {private long id;private String description;private BigDecimal totalCost = BigDecimal.valueOf(0);private List orderLines = new ArrayList();private Customer customer;public Order() {}public void readExternal(ObjectInput stream) throws IOException, ClassNotFoundException {this.id = stream.readLong();this.description = (String)stream.readObject();this.totalCost = (BigDecimal)stream.readObject();this.customer = (Customer)stream.readObject();this.orderLines = (List)stream.readObject();}public void writeExternal(ObjectOutput stream) throws IOException {stream.writeLong(this.id);stream.writeObject(this.description);stream.writeObject(this.totalCost);stream.writeObject(this.customer);stream.writeObject(this.orderLines);}
}
Order对象的可外部化的序列化字节
----sr--model.Order---*3--^---xpw---------psr--java.math.BigDecimalT--W--(O---I--scaleL--intValt--Ljava/math/BigInteger;xr--java.lang.Number-----------xp----sr--java.math.BigInteger-----;-----I--bitCountI--bitLengthI--firstNonzeroByteNumI--lowestSetBitI--signum[--magnitudet--[Bxq-~----------------------ur--[B------T----xp----xxpsr--java.util.ArrayListx-----a----I--sizexp----w-----sr--model.OrderLine-!!|---S---xpw-----pq-~--q-~--xxx
其他序列化选项
我开始研究Java中还有哪些其他序列化选项。 我从EclipseLink MOXy开始,它支持通过JAXB API将对象序列化为XML或JSON。 我并不期望XML序列化能胜过Java序列化,因此在某些用例中确实感到惊讶。 我还找到了产品Kryo,这是一个用于优化序列化的开源项目。 我还研究了Oracle Coherence POF序列化格式。 每个产品都有优点和缺点,但我的主要重点是比较它们的性能和效率。
EclipseLink MOXy – XML和JSON
使用EclipseLink MOXy序列化为XML或JSON的主要优点是两者都是标准的可移植格式。 您可以使用任何语言从任何客户端访问数据,因此与Java序列化一样,不限于Java。 您还可以将数据与Web服务和REST服务集成。 两种格式都基于文本,因此易于阅读。 不需要编码或特殊接口,只需元数据。 性能是完全可以接受的,并且对于小型数据集,其性能优于Java序列化。
缺点是文本格式的效率不如优化的二进制格式,并且JAXB需要元数据。 因此,您需要使用JAXB批注来批注您的类,或提供一个XML配置文件。 另外,默认情况下不处理循环引用,您需要使用@XmlIDREF来处理循环。
JAXB注释的类
@XmlRootElement
public class Order {@XmlID@XmlAttributeprivate long id;@XmlAttributeprivate String description;@XmlAttributeprivate BigDecimal totalCost = BigDecimal.valueOf(0);private List orderLines = new ArrayList();private Customer customer;
}public class OrderLine {@XmlIDREFprivate Order order;@XmlAttributeprivate int lineNumber;@XmlAttributeprivate String description;@XmlAttributeprivate BigDecimal cost = BigDecimal.valueOf(0);
}
订单对象的EclipseLink MOXy序列化XML
<order id="0" totalCost="0"><orderLines lineNumber="1" cost="0"><order>0</order></orderLines></order>
订单对象的EclipseLink MOXy序列化JSON
{"order":{"id":0,"totalCost":0,"orderLines":[{"lineNumber":1,"cost":0,"order":0}]}}
ry
Kryo是一个快速,高效的Java序列化框架。 Kryo是根据New BSD许可提供的Google代码上的开源项目。 这是一个很小的项目,只有3个成员,它于2009年首次发布,最后一次于2013年2月发布2.21版本,因此仍在积极开发中。
Kryo的工作方式类似于Java序列化,并且尊重瞬态字段,但不需要类可序列化。 我发现Kryo有一些限制,例如要求类具有默认构造函数,并且在序列化java.sql.Time,java.sql.Date和java.sql.Timestamp类时遇到了一些问题。
Order对象的Kryo序列化字节
------java-util-ArrayLis-----model-OrderLin----java-math-BigDecima---------model-Orde-----
Oracle Coherence POF
Oracle Coherence产品提供了自己优化的二进制格式,称为POF(便携式对象格式)。 Oracle Coherence是一种内存数据网格解决方案(分布式缓存)。 一致性是一种商业产品,需要许可证。 EclipseLink通过使用Coherence作为EclipseLink共享缓存的Oracle TopLink Grid产品支持与Oracle Coherence的集成。
POF提供了序列化框架,并且可以独立于Coherence使用(如果您已经获得Coherence许可)。 POF要求您的类实现可PortableObject
接口和读/写方法。 您还可以实现单独的Serializer类,或在最新的Coherence版本中使用注释。 POF要求为每个类提前分配一个常量ID,因此您需要某种方式确定此ID。 POF格式是一种二进制格式,非常紧凑,高效且快速,但是您需要做一些工作。
POF的总字节数对于单个Order / OrderLine对象为32字节,对于100 OrderLines为1593字节。 我不会给出结果,因为POF是一种商业许可产品的一部分,但是速度非常快。
POF便携式对象
public class Order implements PortableObject {private long id;private String description;private BigDecimal totalCost = BigDecimal.valueOf(0);private List orderLines = new ArrayList();private Customer customer;public Order() {}public void readExternal(PofReader in) throws IOException {this.id = in.readLong(0);this.description = in.readString(1);this.totalCost = in.readBigDecimal(2);this.customer = (Customer)in.readObject(3);this.orderLines = (List)in.readCollection(4, new ArrayList());}public void writeExternal(PofWriter out) throws IOException {out.writeLong(0, this.id);out.writeString(1, this.description);out.writeBigDecimal(2, this.totalCost);out.writeObject(3, this.customer);out.writeCollection(4, this.orderLines);}
}
Order对象的POF序列化字节
-----B--G---d-U------A--G-------
结果
那么每种表现如何呢? 我做了一个简单的基准比较不同的序列化机制。 我比较了两个不同用例的序列化。 第一个是具有单个OrderLine对象的单个Order对象。 第二个是具有100个OrderLine对象的单个Order对象。 我比较了每秒的平均序列化操作,并测量了序列化数据的字节大小。 不同的对象模型,用例和环境将产生不同的结果,但这使您对不同的序列化器的性能差异有一个大致的了解。
结果表明,Java序列化对于少量对象来说很慢,但是对于大量对象来说很好。 相反,对于少量对象,XML和JSON的性能优于Java序列化,但是对于大量对象,Java序列化的速度更快。 Kryo和其他优化的二进制序列化程序在这两种数据类型方面均优于Java序列化。
您可能想知道,为什么不到一毫秒的时间与性能有任何关系,这可能是相关的,您可能是对的。 通常,如果您写出大量对象,然后Java序列化执行得很好,那么您只会遇到一个实际的性能问题,那么,对于少量对象而言,它的执行效果很差吗? 对于单个操作,这可能是正确的,但是如果执行许多小的序列化操作,则成本是相关的。 为许多客户端提供服务的典型服务器通常会发出许多小请求,因此尽管序列化的成本不足以使这些单个请求中的任何一个花费很长时间,但它将极大地影响服务器的可伸缩性。
用1条订单行订购
序列化器 | 大小(字节) | 序列化(操作/秒) | 反序列化(操作数/秒) | 差异百分比(来自Java序列化) | 差异百分比(反序列化) |
---|---|---|---|---|---|
Java可序列化 | 636 | 128,634 | 19,180 | 0% | 0% |
Java可外部化 | 435 | 160,549 | 26,678 | 24% | 39% |
EclipseLink MOXy XML | 101 | 348,056 | 47,334 | 170% | 146% |
ry | 90 | 359,368 | 346,984 | 179% | 1709% |
订购100条订单行
序列化器 | 大小(字节) | 序列化(操作/秒) | 反序列化(操作数/秒) | 差异百分比(来自Java序列化) | 差异百分比(反序列化) |
---|---|---|---|---|---|
Java可序列化 | 2,715 | 16,470 | 10,215 | 0% | 0% |
Java可外部化 | 2,811 | 16,206 | 11,483 | -1% | 12% |
EclipseLink MOXy XML | 6,628 | 7,304 | 2,731 | -55% | -73% |
ry | 1216 | 22862 | 31,499 | 38% | 208% |
EclipseLink JPA
在EclipseLink 2.6开发版本(在某种程度上为2.5)中,我们增加了在EclipseLink进行序列化的任何地方选择序列化程序的功能。
这样的地方之一是序列化@Lob映射。 现在,您可以使用@Convert批注指定序列化程序,例如@Convert(XML),@ Convert(JSON),@ Convert(Kryo)。 除了优化性能之外,这还提供了一种简单的机制来将XML和JSON数据写入数据库。
同样对于EclipseLink缓存协调,您可以使用“ eclipselink.cache.coordination.serializer”属性选择序列化器。
这篇文章中使用的基准测试的源代码可以在这里找到,或者在这里下载。
翻译自: https://www.javacodegeeks.com/2013/09/optimizing-java-serialization-java-vs-xml-vs-json-vs-kryo-vs-pof.html