原文

项目中要测一下 jackson vs gson 的性能对比, 所以就查找了下 jackson 的性能方面的资料.发现官方文档就有很好的介绍了.特此翻译, 以备下次再查用.

Jackson 的涡轮增压

尽管 Jackson 的 JSON 处理器 默认情况下的设置和通常的使用模式在开箱即用下已经很快了, 但这有几个方式来使它处理得更快.

这篇描述关注几点你可以用来使用的东西, 它们在性能方面有很大的不同, 特别是在用尽CPU性能的地方.

基础: 无论如何你都应该做的事

有一些要遵循的基本规则, 以确保 Jackson 是以最佳的方式来处理东西的. 这是一些你无论如何都应该做的, 即使你没有实际的性能问题: 你可以把它们看作是 “童子军规则” 的解释(永远不要让露营地比你发现的要干净). 请注意, 以下指南的重要性顺序是递减的:

  1. 重用一些 重对象 (heavy-weight objects): ObjectMapper (data-binding) 和 JsonFactory (streaming API).

    1. 更轻一点的对象: ObjectReaderObjectWriter , 这些仅是一些锦上添花, 但它们完全是线程安全的并且是可重用的.
  2. 关闭一些需要关闭的东西: JsonParser, JsonGenerator.

    1. 这有且于重用底层的东西, 如 symbol table, 可重用的 input/output buffers
    2. 对于 ObjectMapper 来说没有什么东西要关闭的
  3. 使用未提炼(最少处理)的形式作为 input(输入), 例如, 不要尝试装饰输入源和输出目标:

    1. Input: byte[] 是最好的, 如果你有的话; InputStream 是次好的; 然后到 Reader – 在任何情况下, 不要尝试读入 inputString.
    2. Output: OutputStream 是最好的, 其次到 Writer; 并且调用 writeValueAsString() 是效率最低的(为什么构造一个中间的 String ?)
    3. 解释: Jackson 是非常擅长发现最有效(有时是 zero-copy) 的方式来消费/产生 JSON 编码的数据的 – 让它自己来做这个魔法吧.
  4. 如果你需要: 重复处理, 然后回放, 那不要重复解析.

    1. 有时, 你在多个阶段里需要处理一些东西; 例如, 你可能需要解析部分的 JSON 编码的数据到更深入的处理或数据绑定的规则, 并且/或 修改中间的表示对象来进行更一步的处理
    2. 不要将中间的形式写回 JSON (这会导致 JSON 写入和读取的开销), 最好使用更高效的中间形式
    3. 最高效的中间形式是 TokenBuffer (JSON token 的平坦序列); 接着的是 JSON 的树模式 (JsonNode)
    4. 也许想使用 ObjectMapper.convertValue() , 在对象类型之间转换.
  5. 使用 ObjectReader 的方法 readValues() 来读取相同的 POJO 类型的序列

    1. 功能等同于多次调用 readValue(), 但它们都更方便并且(轻微地)更高效
  6. ObjectReaderObjectMapperObjectMapper 更好

    1. ObjectReaderObjectWriter 都是可以安全使用的 – 它们完全是不可变的并且可安全地在线程之间使用 – 但它们也会更高效, 因为它们可以避免一些查找, 而这些查找对 ObjectMapper 来说是必需的.

更深入提高性能的特殊选项

一旦你查看了上面讨论的基础知识, 你可能需要考虑其他旨在进一步提升性能的工作.

轻松 VS 兼容性

有两种主要的标准来区分它们:

  1. 轻松 – 有多少工作涉及修改
  2. 兼容性 – 由此产生的系统, 与普通旧式的JSON用法的互操作性?

兼容, 但不那么容易: 使用 Stream API

Jackson Databind API 的最大受益点就是使用上比较轻松简单: 仅一两行代码就可以从 POJOJSON 之间相互转换. 但这些所谓的方便并不是完全免费的: 在一些自动处理方面会有一些额外的开销, 例如处理 POJO 的属性时使用 Java 的反射(相比于显式调用 getter 和 setter).

因此, 一个直接的(如果很费力的话)可能是重写数据转换以使用 Jackson Streaming API. 使用它必须构建 JsonParserJsonGenerator, 并且使用低级调用来读取和写入JSON 作为 token.

如果你显式在重写所有的转换为 Streaming API 以代替 Data Binding, 你可能可以提升 30%~40% 的吞吐率, 并且它没有改变实际的 JSON 处理. 但是写和维护低级代码需要花费时间和精力.所以你是否想要这样子做取决于你想投资多少来实现适度的性能提升.

一种可能的折衷方案是只重写处理的一部分; 特别是优化最常用的转换. 这通常是叶子级的类(只有原始或字符串属性的类). 你可以通过为少类类型编写 JsonSerializerJsonDeserializer 来实现此目的. Jackson 会很高兴地使用它自己默认的 POJO 的序列化和反序列化器, 并且对特定类型的自定义覆盖它们.

不兼容, 但容易: Smile binary JSON

另一种折衷是考虑使用 Smile Binary JSON 格式, 它是从 Jackson 1.6 开发的.

Smile 是一种二进制格式, 与逻辑JSON数据模型 100% 兼容; 类似于 binary XML (如 Fast Infoset), 它与标准的 XML 相关.这意味着 JSON 和 Smile 之间的转换可以高效完成, 且不会丢失任何信息. 这也意味着使用 Smile 编码数据的 API 几乎与常规的 Jackson API 相同: 唯一的区别是底层工厂的类型是 SmileFactory, 而不是 JsonFactory. SmileFactory 由 Jackson Smile Module 提供.

使用 Smile 来转换 Service (或 Client)是很容易的: 只需创建一个使用 SmileFactory 的 ObjectMapper 即可.但潜在的挑战是客户可以看到这样的变化; 它可能是也可能不是问题(取决于内容格式是否可以自动协商, 如使用 JAX-RS 所做的那样). 但这是一个明显的变化.

二进制格式的使用也可能普遍地存在问题; 处理二进制格式对于 JavaScript非常困难(对于所有二进制格式, 包括 protobuftrift 都是如此). 对于 JS, 具体来说, 它比处理 JSON 更慢 – 但也可能会造成语言问题, 这些语言还没有可用的 Smile 编码解码器. 目前 Smile 支持由 C 语言编写的 libsmile 库提供(显然是标准的 Java 实现).最后, 二进制格式的调用比文本数据格式更难, 因为需要某种类型的读取器.

使用 Smile 的性能改进类似于使用 Streaming API (提高 30%~50%), 但额外的好处是数据的大小会减少, 通常是减少 30%~50% . 请注意, 性能改进对于像类似对象流(大数据, 比如 Map/Reduce 数据流)的冗余数据更为重要. 这是因为 Smile 可以使用对所有的反向引用, 但是消除重复的属性名称和短字符串值(如枚举值).

最后, 请注意, 与使用 JSON 一样, 使用 Smile 作为底层格式时, 你还可以在 Streaming API 和 Databinding 之间进行选择. 这样做结合了多种提高性能的策略.

不兼容, 但容易: POJO 作为 JSON Arrays (Jackson 2.1)

即将发布的 Jackson 2.1 中即将推出新选项可以改变用于序列化 Java 对象的实际 JSON 结构. 例如, 考虑一个假设的 Point class 的情况:

public class Point {
  public int x, y;
}

它通常会序列化为:

{"x":27, "y":15}

然而, 如果声明为这样子:

@JsonFormat(shape=JsonFormat.Shape.ARRAY)
@JsonPropertyOrder(alphabetic=true)
public class Point {
  public int x, y;
}

则会序列化为

[27,15]

它基本上通过使用位置值来指示哪些属性值存储在哪里, 从而消除了属性名称. 这可能会导致序列化 JSON 内容的大小显著地减小; 并且这转换直接会影响性能.还值得注意的是, 这对于简单非重复数据(如请求/响应消息)同样适用. 因为属性名被简单地消除.

和 Smile 一样, 这种变化对客户来说是直接可见的, 要么客户端使用 Jackson, 要么它们实现类似的功能. 尽管如此, 这种格式稍微容易阅读(或至少是调试)和使用脚本语言进行处理.

兼容, 并且容易: Afterburner

经过几次妥协(简单或兼容)后, 有一种方法即简单又兼容: Jackson Afterburner Module.

Afterburner 模块优化了底层的序列化和反序列化器:

  • 使用字节码生成替代实际字节码的Java反射调用(用于字段和方法访问和构造函数调用) – 类似于如何从Java代码编写显式字段访问和方法调用.
  • 内联处理一小组基本类型(String, int, long – 将来可能会更多). 因此如果使用默认的序列化/反序列化器, 则调用将被等效的标准处理所取代(这会消除一些方法调用, 和可能的参数/返回值装箱)
  • 使用 JsonParser 中的特殊匹配调用推测 “匹配/解析” 有序属性名称 – 如果字段名按照 Jackson 序列化的顺序进行序列化, 则可消除符号表查找(可以使用 @JsonPropertyOrder 指示)

由于这些优化或多或少模仿了”手写”转换器所使用的更高效模式(即我们的第一个选项, 使用 Streaming API), 因此性能改进理论上可以达到此类转换器的水平. 实际上, 我们已经观察到这个最大值的 60%~70% 范围内的改进(即, Afterburner 可以消除标准数据绑定超过手写替代器的 23 的开销)

方法的成熟度

迄今为止讨论的方法有不同的成熟度水平, 这可能会影响你的决策过程:

  • Streaming API – 基于转换器(手写): 在第一版的 Jackson 就可用了.
  • Smile Format – 首次在 Jackson 1.6 , 非常稳定, 格式和解析器/生成器的实现.(Elastic Search 中大量使用 Smile)
  • Afterburner: 首次在 Jackson 1.8, 也非常稳定.
  • POJO-as-array: 首次在 Jackson 2.1

或者做以上所有(几乎)的选项!

但是你需要选择一种方式吗? 绝对不.

事实上, 将多种方法结合起来可以带来巨大的回报, 并且可以将大多数方法结合起来. 特别是:

  • 选择 Smile 来替代 JSON, 对于其他的选项是兼容的, 并且可以独立变化.
  • POJO as array 和 Afterburner 的选择与 Streaming API 以外的选项兼容.

因此, 你可以考虑的组合是:

  • 使用 Smile 格式, 但写你自己的代码来用 Streaming API . 这也是一些框架做的事, 如 (Elastic Search)
  • 使用 AfterburnerPojo as array: 既可是 JSON , 也可是 Smile.

这种组合有且于达到最佳性能.

它到底有多快?

对于上述那些极端组合, 使用普通的旧JSON可以达到或超过诸如 Protobuf, thrift 或 avro 等快速二进制格式的性能. 而使用 Smile , 处理速度和数据大小可以超过其他选择(体积小, 甚至更快)

尽管并非所有上述组合都包含在内, 但 JVM 序列化基准测试可以提供一些改进的想法, 因为它包含了 JSON/Smile, Streaming API/data binding/ Afterburner 组合的结果.