首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一次 Javac 编译速度缓慢的 JDK Bug 定位

一次 Javac 编译速度缓慢的 JDK Bug 定位

原创
作者头像
用户7118204
修改2020-04-07 14:23:21
7350
修改2020-04-07 14:23:21
举报

背景

Flink 提供了从 Tuple0 ~ Tuple25 的 Tuple 类供用户选择,顾名思义,每个 Tuple 对象分别可以存储 0 个 ~ 25 个任意类型的字段,例如图 1 展示了 Tuple2 的类定义。由于腾讯云 Oceanus 流计算的客户业务场景较为复杂,需要用到更高维度的固定 Tuple 类,我们将 Tuple 类进一步扩展,达到了 Tuple250 甚至 Tuple500.

图 1:Tuple2 的类定义,有着 f0、f1 两个泛型字段

但是,随着 Tuple 维度的增多,我们观察到了一个诡异的现象:虽然需要编译的源码文件增加个数不多,但是编译所需时间越来越长,且并非线性增长:原本只需要一分钟就可以完成的编译,现在需要动辄一个多小时;如果在本机进行编译,甚至几个小时都编译不完。这给我们的开发效率带来了一定程度的影响,因此有必要找出问题根源。

初探

为了找出 Tuple 数与编译时间的关系,我们还写了一个自动化脚本,每次向源码里增加 1 个更高维度的 Tuple 类(例如依次放入 Tuple26.java、Tuple27.java 等等),观察项目的构建速度,并绘制了如下的曲线(图中公式使用 Excel 的趋势线进行拟合),见下图 2:

图 2:Tuple 总数与编译时间的关系

可以看到,编译时间随 Tuple 数变化的曲线,完美符合三次函数,即该算法的时间复杂度约为 O(n^3)。如此高的时间复杂度,一定要找出根源,否则随着业务规模的进一步扩大,编译时间会越来越难以接受。

为了解决这个问题,我们首先想到的是使用 Profiling 工具进行热点和调用时长的统计分析。这里选择了 JProfiler,它提供了很多有用的分析视图,可以迅速找到问题的直接根源。

首先我们对编译缓慢的项目启动编译构建,默认情况下是基于 Maven 的,因此需要找出是不是 Maven 导致的问题。我们采用的 JDK 版本是 1.8.0_202.

图 3:使用 Sampling 模式对 Maven 编译的进程进行采样

首先我们使用 JProfiler 的 Sampling 模式进行采样(如图 3),它的效果类似于不断地运行 jstack 命令,不进行侵入式的修改,因此得到的数据较为准确;另一种 Instrumentation 模式适合于找到问题的热点后,使用 JVMTI 动态修改字节码机制(线上定位神器 btrace 也是基于这个原理),进行局部的细致分析。需要注意的是,默认情况下采样排除了 JVM 内部的调用,我们由于需要定位 JDK 的问题,需要在 Call tree filters 里把所有的排除规则清空,否则问题只能定位到 Maven 这一层。

当程序运行一段时间后,我们找出了热点方法(见图 4),即 javac 编译起内部的 List 相关调用;通过仔细追踪调用链,发现是 checkWithinBounds 方法过于缓慢。

图 4:找出热点方法

既然热点方法找到了,那么下面就需要探究这个方法在 javac 编译器中是做什么的,它的算法为什么这么慢,以及是否有优化的方式。

详细定位

由于调用链里有 Infer 类,我们知道它是负责泛型的类型推断的。通过搜索泛型编译缓慢等关键字,找到了 JDK-8086048 这个 Bug 单,同时在 JDK-8080656 这里也有提到同样的问题。

随后我们又跟踪到了 JDK-8051946JEP-215。在这个 2014 年就提出的 JEP-215 中,开发者设计了一种新的 javac 方法类型检测机制 TA(Tiered Attribution)来代替现有的 SA(Speculative Attribution),可以极大加速多态表达式(Poly Expression)的检查过程。

通过阅读这个 JEP(JDK Enhancement Proposal)的描述,可以知道目前的 SA 算法需要在同一颗语法树上,对多个不同的目标进行多次类型检查,例如一个 多态表达式有 N 种重载选项,那么就需要检查 N * 3 + 1 次。如果参数还允许嵌套的话,那么多个因子还会相乘,这样就导致了我们上述遇到的很高的时间复杂度了。

而这个新的 TA 算法提供了一种更高效的多态表达式类型检查机制。例如省去了重载解析过程的类型检查,并于重载解析前,为每一个方法调用过程中的多态参数表达式(poly argument expression)构造解析所需的自底向上结构化类型,以大大减少总的尝试次数。

根据这些 Bug 单,JEP-215 已经在 JDK 9 及更高版本上得以实现。因此我们改用当前已发布的 LTS 版本 JDK 11 进行验证。

通过修改 JAVA_HOME 环境变量,可以让 Maven 选择使用不同的 JDK 版本进行编译,我们修改为 JDK 11 的路径后,重新进行编译,并再次进行采样,结果发现类型推断已经不再是占用 CPU 最多的方法了(图 5):

图 5:改为 JDK 11 编译时的热点方法

同时我们欣喜地发现,整个项目只需要 1.5 分钟就构建完毕了,相对之前的 1 个多小时,有了质的飞跃(图 6):

图 6:新版本 JDK 的编译耗时

由此可见,这个 JEP-215 起到了立竿见影的效果,让项目构建的时间恢复了往日的情景。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 初探
  • 详细定位
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档