专栏首页腾讯云流计算一次 Javac 编译速度缓慢的 JDK Bug 定位
原创

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

背景

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 起到了立竿见影的效果,让项目构建的时间恢复了往日的情景。

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

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

我来说两句

0 条评论
登录 后参与评论

相关文章

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

    Flink 提供了从 Tuple0 ~ Tuple25 的 Tuple 类供用户选择,顾名思义,每个 Tuple 对象分别可以存储 0 个 ~ 25 个任意类型...

    用户7118204
  • Javac 编译速度缓慢的 JDK Bug 定位

    Flink 提供了从 Tuple0 ~ Tuple25 的 Tuple 类供用户选择,顾名思义,每个 Tuple 对象分别可以存储 0 个 ~ 25 个任意类型...

    用户7118204
  • 当年偶然发现的 Java Bug(JDK 9及之前仍未修复)

    15年在中信银行做持续集成时,由于当时的项目是基于三方采购的 Java 配置开发平台做的,平台自己基于 Ant 插件实现了增量和热部署。

    叨叨软件测试
  • 【2021年】小哪吒入门学习Java的第一天

    初级学习方向:Java入门,idea开发环境使用(idea,eclipse),面向对象基础和面向对象设计,Java虚拟机内存机制,内存分析全面理解面向对象,内存...

    达达前端
  • 面试官喜欢问的Java编译期与运行期问题总结全了

    不知大家有没有思考过,当我们使用IDE写了一个Demo类,并执行main函数打印 hello world时都经历了哪些流程么?

    业余草
  • 90% 的 Java 程序员都说不上来的为何 Java 代码越执行越快(1)- JIT编译优化

    经常听到 Java 性能不如 C/C++ 的言论,也经常听说 Java 程序需要预热,那么其中主要原因是啥呢?

    干货满满张哈希
  • JVM系列第4讲:从源代码到机器码,发生了什么?

    在上篇文章我们聊到,无论什么语言写的代码,其到最后都是通过机器码运行的,无一例外。那么对于 Java 语言来说,其从源代码到机器码,这中间到底发生了什么呢?这就...

    陈树义
  • 你以为反射真的不能为所欲为?至少JDK8以后很强

    这里就不在赘述如何通过Method对象调用方法了。文章末尾会给出上一章节的地址。今天我们要研究的是Method如何获取方法参数这一块。看似简单却又是那么的传奇。...

    IT大咖说
  • 《Java程序设计基础》 第2章手记

    前言: 本手记以大学Java教学书籍《Java程序设计基础》第四版(清华大学出版社 陈国君等编著)为背景,每周更新一次,内容涉及学习这本书中可能遇到的问题及其解...

    Steve Wang

扫码关注云+社区

领取腾讯云代金券