为什么 Spring6 强制要求 JDK17

当然 JDK 的升级有很多种原因,本文主要从 Spring6 的一大新特性 AOT 来探讨。

alt

图1的横坐标代表应用执行的时间顺序,纵坐标代表CPU利用率,各个颜色的区域代表该行为的CPU使用率,红色区域的VM表示JVM、青色的CL代表类加载(Class Loading),白色的是实时编译(Just In Time,JIT),黄色的代表垃圾回收(GC),浅绿色代表解释执行应用程序,绿色代表执行经过JIT编译的应用代码。

从图1中可以看到各个阶段中花费时间最多的行为是什么,但这里的使用情况并不是按实际比例绘制的,而是只反映整体趋势的示意,因为具体的数据会随应用不同而变化。

从图1可以看到Java程序的运行生命周期是:首先启动JVM,执行各种VM的初始化动作;然后调用Java程序的主函数进入应用初始化,此时才会开始通过解释执行方式运行Java代码,随着Java代码运行而同时开始的还有GC,JIT会在出现热点函数时才开始;当程序初始化完成后,开始执行应用程序的业务代码,此时才算进入了程序执行的预热阶段,这个阶段会有大量的类加载和JIT编译行为;当程序被充分预热后,就进入了运行时性能最好的稳定阶段,此时的理想状态是只有应用本身和GC在运行,其他的行为都已渐渐退出;最后是关闭应用,各个行为次第结束。

Java 语言最初被认为是一种解释型语言,因为 Java 源代码并非被先编译为与机器平台相关的汇编代码再执行,而是先编译为与平台无关的字节码(bytecode),然后由 JVM 解释执行。

解释执行是由 JVM 将字节码逐条翻译为汇编代码,然后执行的过程。经过解释的代码缺少编译优化,因此运行时性能较低。不过解释执行非常灵活,可以支持诸如动态类加载这样的动态特性。Java 可以在运行时解释执行一段在编译时尚不存在的代码,这种特性对于编译执行类型的语言来说是难以想象的。

为了解决运行时性能低的问题,Java 引入了实时编译技术(JIT,Just In time),在运行时将热点函数编译为汇编代码,当程序再次运行到经过实时编译的函数时,就可以执行经过编译和优化的汇编代码,而不再需要解释执行了。由于编译是在运行时进行的,因此 JIT 编译器可以获得代码实际运行的路径、热点和变量值等信息,基于此可以做出非常激进的编译优化,从而获得执行效率更高的代码。

OpenJDK使用的JIT编译器分为C1(client)和C2(server),C1编译器通常用于快速启动和简单的应用程序,因为它生成的代码速度较快,但优化程度较低。而C2编译器更加激进,会花费更多时间进行更深层次的优化,生成更高效的本地机器代码,适用于需要更高性能的场景。

现在的Java程序基本都是采用解释执行加JIT执行的混合模式,当函数执行次数较少时解释执行,而当函数的执行次数超过一定阈值后再JIT执行,从而实现了热点函数JIT 执行、非热点函数解释执行的效果。

不过虽然JIT带来了非常显著的性能优势,但因为编译优化本身是需要占用系统资源的资源密集型运算,它会影响应用程序的运行时性能,在实践中甚至出现过JIT线程占用过多资源,导致应用程序不能执行的状况。此外,如果代码执行的次数较少,编译优化代码造成的性能损失可能会大于编译执行带来的性能提升。

所以冷启动问题的原因有两点: 一是Java的虚拟机模型机制,二是从解释执行到JIT执行的分层次执行模型。这两点在当前的Java模型下是无法更改的,它们都是Java运行时的基石。

alt

如何解决冷启动问题

Java虚拟机的主要作用是提供跨平台能力,以支持与平台无关的Java字节码可以在不同的操作系统中运行。解释执行、JIT执行等问题都是由此衍生而来的。如果我们并不需要跨平台能力,是不是可以将Java程序直接编译为目标平台的机器码,然后提供必要的运行时支持,让它以操作系统原生程序的形式运行?

这就是静态编译技术。

Java静态编译是指将Java程序的字节码在单独的离线阶段编译为汇编代码,其输入为Java的字节码,输出为操作系统本地原生程序。“静态”是相对传统Java程序的动态性而言的,因为传统Java程序是在运行时动态地解释执行和JIT编译,而静态编译需要在执行前就静态地完成程序的编译。

GraalVM项目中就提供了Java静态编译所需的编译工具链、编译框架、编译器和运行时等全套支持。 Oracle在18年官宣了GraalVM的1.0版本。虽然名字里带着VM,但实际上它既是 HotSpot 的新型 JIT 编译器,又可以用作AOT编译器,也是一个新的多语言虚拟机。GraalVM有3个关键的组件:

Graal - 用Java写的编译器,既可以作为 JIT 编译器取代C2在传统的OpenJDK JVM上运行,又可以当做AOT编译器使用。 Graal Compiler是GraalVM与HotSpotVM(从JDK10起)共同拥有的服务端即时编译器,是C2编译器未来的替代者。为了让 Java 虚拟机与编译器解耦,ORACLE引入了Java-Level JVM Compiler Interface(JVMCI),把编译器从虚拟机中抽离出来,并且可以通过接口与虚拟机交流。部署在Substrate VM 上。

alt

具体来说,即时编译器与 Java 虚拟机的交互可以分为如下三个方面。

  1. 响应编译请求;
  2. 获取编译所需的元数据(如类、方法、字段)和反映程序执行状态的 profile;
  3. 将生成的二进制码部署至代码缓存(code cache)里。 alt

Substrate VM - 是一个构建在Graal编译器之上的,支持AOT编译的运行框架。它的设计初衷是提供一个快速启动,低内存占用,以及能无缝衔接C代码(与JNI相比)的runtime,并能完美适配Truffle语言实现。 Substrate VM简单来说就是native image builder + SubstrateVM Runtime,分别对应原生镜像(Native Image)的build time和run time。

native image builder:使用Graal编译器做静态编译的工具,它处理应用程序的所有类和依赖项(包括来自JDK的部分),通过指针分析(Points-To Analysis)来确定在应用程序执行期间可以访问哪些类和方法,然后提前将可访问的代码和数据编译为特定操作系统和架构的可执行文件或者动态链接库。

SubstrateVM Runtime:一个特殊的精简过的VM Runtime,包括了deoptimizer、GC、线程调度等组件。因为已经做了AOT编译,比传统的Runtime少了类加载、解释器、JIT等组件。

alt

Truffle - 即下图中的语言实现框架(Language Implementation Framework),用来支持多种语言跑在GraalVM上。

alt alt

曾经 Spring 的流行,轻量化也是一大优点,但在云原生、serverless 的趋势下,一个 Springboot 项目的启动速度、内存占用已经谈不上轻量了,也涌现一批竞品,如上图中出现的开源社区涌现了Quarkus、Micronaut、Helidon等一批以提升 Java 在云原生环境下的适应性为卖点的微服务框架,但Spring 又是一个动态性很强的框架,其核心的IoC和AOP功能大量使用了反射、动态字节码生成等技术,而GraalVM的静态编译的基本原则是封闭性假设(closed world assumption),要求编译器在编译时必须掌握运行时所需的全部信息,换句话说,就是运行时不能出现任何编译时未知的内容。这是因为应用程序的可达范围在静态编译时被限定了,因为没有了类加载器、解释器等组件,不能在运行时解析和执行任何动态引入的类。这与Spring的动态性的是冲突的。 因此Spring 依赖 Graal 设计 AOT plugin 模块。

alt

回到主题,我认为因为 Spring 框架已经决定要继续向轻量化靠拢,就不得不舍弃一部分兼容性,而 jdk17 是继jdk8 后最长的 LTS,各种新特性已比较稳定,性能也提升了不少,大部分第三方的框架,库都已支持,因此做出了这个决定。

全部评论

相关推荐

03-19 09:58
河海大学 Java
最喜欢春天的奇亚籽很...:同学,是小红书不是小哄书,一眼就能看到的错误
投了多少份简历才上岸
点赞 评论 收藏
分享
评论
3
3
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务