总览
有关在Java和低延迟中使用Lambda的主要问题是: 它们会产生垃圾吗,您能做些什么吗?
背景
我正在开发一个支持不同有线协议的库。 这样的想法是,您可以描述要写入/读取的数据,并且有线协议确定它是否使用带有JSon或YAML字段的文本,带有FIX字段号的文本,带有BSON字段名的二进制或YAML的二进制形式,具有字段名称,字段编号或完全没有字段meta的二进制。 这些值可以是固定长度,变量长度和/或自描述数据类型。
其想法是它可以处理各种模式更改,或者如果您可以确定模式是相同的(例如,通过TCP会话),则可以跳过所有内容而仅发送数据。
另一个大想法是使用lambda支持这一点。
Lambdas有什么问题
主要问题是需要在低延迟应用程序中避免大量垃圾。 名义上,每次您看到lambda代码时,这都是一个新对象。
幸运的是,Java 8大大改进了Escape Analysis 。 Escape Analysis使JVM通过将新对象解包到堆栈中来替换它们,从而有效地为您分配了堆栈。 Java 7中提供了此功能,但是很少消除对象。 注意:使用探查器时,它往往会阻止Escape Analysis正常运行,因此您不能信任使用代码注入的探查器,因为探查器可能会说正在创建对象,而没有探查器则不会创建对象。 Flight Recorder似乎确实与Escape Analysis混为一谈。
Escape Analysis一直都有古怪之处,而且看来仍然如此。 例如,如果您具有IntConsumer或任何其他原始使用者,则可以在Java 8 update 20 – update 40中消除lambda的分配。但是,在没有发生这种情况的情况下,布尔值是一个例外。 希望它将在将来的版本中修复。
另一个怪癖是,对象消除发生的方法的大小(内联之后)很重要,在相对适度的方法中,转义分析可以放弃。
具体情况
就我而言,我有一个读取方法,如下所示:
public void readMarshallable(Wire wire) throws StreamCorruptedException {wire.read(Fields.I).int32(this::i).read(Fields.J).int32(this::j).read(Fields.K).int32(this::k).read(Fields.L).int32(this::l).read(Fields.M).int32(this::m).read(Fields.N).int32(this::n).read(Fields.O).int32(this::o).read(Fields.P).int32(this::p).read(Fields.Q).int32(this::q).read(Fields.R).int32(this::r).read(Fields.S).int32(this::s).read(Fields.T).int32(this::t).read(Fields.U).int32(this::u).read(Fields.V).int32(this::v).read(Fields.W).int32(this::w).read(Fields.X).int32(this::x);
}
我使用lambda来设置框架可以处理可选,缺失或乱序的字段。 在最佳情况下,可以按提供的顺序使用字段。 在模式更改的情况下,顺序可以不同或具有不同的字段集。 使用lambda可使框架以不同方式处理顺序字段和乱序字段。
使用此代码,我进行了测试,对对象进行了1000万次序列化和反序列化。 我将JVM配置为具有-Xmn14m -XX:SurvivorRatio=5
的eden大小为10 MB的伊甸园空间-Xmn14m -XX:SurvivorRatio=5
空间是比率为5:2的两个生存空间的5倍。 Eden空间是年轻一代总数的5/7,即10 MB。
通过具有10 MB的Eden大小和1000万次测试,我可以通过计算-verbose:gc
打印的GC的数量来估计产生的垃圾。对于我得到的每个GC,每个测试平均要创建一个字节。 当我改变序列化和反序列化的字段数量时,我在Intel i7-3970X上获得了以下结果。
在此图表中,您可以看到,对于以相同方法反序列化的1到8个字段(即最多8个lambda),几乎不会创建垃圾,即最多只有一个GC。 但是,在9个或更多字段或Lambda上,转义分析失败,并且您将创建垃圾,垃圾随文件数的增加而线性增加。
我不希望您相信8是一个神奇的数字。 尽管我找不到这样的命令行设置,但它很可能是方法字节数的限制。 当方法增长到170字节时,会发生差异。
有什么可以做的吗? 最简单的“修复”方法是将一种方法中的一半字段反序列化,将另一种字段中的一半字段反序列化,从而将代码分为两种方法(如果需要,可以将更多方法拆分),从而能够在不产生垃圾的情况下反序列化9到16个字段。 这是“ bytes(2)”和“ ns(2)”的结果。 通过消除垃圾,代码的平均运行速度也更快。
注意:使用14 x 32位整数对对象进行序列化和反序列化的时间少于100 ns。
其他说明:
当使用事件探查器YourKit(在这种情况下)时,由于Escape Analysis失败,没有产生垃圾的代码开始产生垃圾。
我打印了方法内联 ,发现某些关键方法中的assert语句阻止了它们的内联,因为这使方法变大了。 我通过在启用断言的情况下通过工厂方法创建断言的方式来创建by main类的子类来解决此问题。 默认类没有断言,也没有性能影响。
在移动这些断言之前,我只能反序列化7个字段而不会触发垃圾回收。
当我用匿名内部类替换lambda时,我看到了类似的对象消除,尽管在大多数情况下,如果可以使用首选的lambda。
结论
Java 8似乎在清除寿命很短的对象产生的垃圾方面要聪明得多。 这意味着在低延迟应用程序中可以选择诸如传递lambda之类的技术。
编辑
尽管我不确定为什么,但我找到了在这种情况下有用的选项。
如果我使用选项-XX:InlineSmallCode=1000
(默认值)并将其更改为-XX:InlineSmallCode=5000
则上面的“已修复”示例开始产生垃圾,但是如果将其减少为-XX:InlineSmallCode=500
甚至是代码我最初给出的示例不产生垃圾。
翻译自: https://www.javacodegeeks.com/2015/01/java-lambdas-and-low-latency.html