Java 大杀器来了!性能提升一个数量级

简介: 自 1996 年诞生以来,Java 语言长期在最受欢迎的编程语言排行榜中占据领先地位。除了语言本身的优秀特性之外,Java 语言持续演进、不断发展也是它能够保持长盛不衰的重要原因。Java 语言的功能和性能都在不断地发展和提高,但是 冷启动开销较大 的问题长期存在,难以从根本上解决。Java 语言也因此在 Serverless 场景下无法与 Node.js、Go 等快速启动的语言竞争,落于下风。在这种背景下,作为能够从根本上解决冷启动问题的 Java 静态编译技术 有了用武之地,开始在业界崭露头角,为 Java 语言注入了新的竞争力。

自 1996 年诞生以来,Java 语言长期在最受欢迎的编程语言排行榜中占据领先地位。除了语言本身的优秀特性之外,Java 语言持续演进、不断发展也是它能够保持长盛不衰的重要原因。Java 语言的功能和性能都在不断地发展和提高,但是 冷启动开销较大 的问题长期存在,难以从根本上解决。


Java 语言也因此在 Serverless 场景下无法与 Node.js、Go 等快速启动的语言竞争,落于下风。在这种背景下,作为能够从根本上解决冷启动问题的 Java 静态编译技术 有了用武之地,开始在业界崭露头角,为 Java 语言注入了新的竞争力。


什么是冷启动?


所谓冷启动问题是指 Java 应用并不是即起即用的,而需要经过虚拟机初始化后才能达到可用状态,再经过程序预热才能达到最佳性能。图 1 给出了 Java 程序的运行时性能随运行时间(实际上是代码重复执行次数)的变化示意图。

b8d324ee094a8c7d5322c47a407a0574.jpg

图 1 Java程序运行时性能演化示意图


  • 横坐标 :程序运行时间,时间越长代表程序中代码被重复执行的次数越多;
  • 纵坐标 :程序的响应时间,响应越快代表运行时性能越好。


可以看出程序响应能力分成了四个部分:第一个阶段为无穷大,因为程序启动时需要首先初始化 Java 虚拟机,然后初始化应用程序,在这个阶段应用是不会有响应的。随后经过解释执行、C1 实时编译和 C2 实时编译,应用的响应时间才从高、中到了低,最终进入稳定执行阶段。前三个阶段就是冷启动,也可以看作程序预热,最后一个阶段为稳定执行,此时的程序运行时性能最好。


在传统的单机或者服务器部署的场景中,冷启动问题并不明显,一来是应用执行时间足够长,冷启动问题就被淡化了;二来人们还可以提前将服务预热准备好,以最好的状态迎接用户的服务请求。


但是在云原生 Serverless 应用的场景中,首次请求必须经过无响应阶段,才会落在响应时间高的为位置,后续请求也会落在高的阶段,只有经过足够多的请求后才会逐渐落入稳定阶段。冷启动问题使得 Java 在 Serverless 场景下无法与 Node.js、Go 等具有快速启动优势的的语言的竞争中,落于下风。


冷启动问题的根本原因是什么?


当我们执行一个 Java 应用程序时,看似是从主函数(Java 可执行应用程序的入口是主函数)开始的,但实际需要在 JVM 初始化后才会调用 Java 主函数开始执行应用程序。


我们将图 1 展示的抽象模型进一步细化,可以得到如图 2 所示的 Java 程序的执行生命周期模型:Java 程序可以分为 VM 初始化(VM init)、应用初始化(App init)、应用预热(App active warmup)、应用稳定(App active steady)和关闭(shutdown)这 5 个阶段。

3162a6b4a70beb4519ece0f91eaa80a2.jpg

图 2 Java应用程序的运行生命周期示意图


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

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


从图 2 可以看到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 和 C2,前者编译优化较少,但是编译所消耗资源也较少;后者编译得到的代码性能最好,但是编译消耗的资源也较多。


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


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


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


如何解决冷启动问题?


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


答案是肯定的,这就是Java 的静态编译技术

Java 静态编译是指将 Java 程序的字节码在单独的离线阶段编译为汇编代码,其输入为 Java 的字节码,输出为操作系统本地原生程序。“静态”是相对传统 Java 程序的动态性而言的,因为传统 Java 程序是在运行时动态地解释执行和 JIT 编译,而静态编译需要在执行前就静态地完成程序的编译。目前由 Oracle 开发的高性能跨语言运行时框架开源项目 GraalVM 中就提供了 Java 静态编译所需的编译工具链、编译框架、编译器和运行时等全套支持,并且已经达到了生产可用的程度。


GraalVM 的静态编译的基本原则是封闭性假设(closed world assumption),要求编译器在编译时必须掌握运行时所需的全部信息,换句话说,就是运行时不能出现任何编译时未知的内容。这是因为应用程序的可达范围在静态编译时被限定了,因为没有了类加载器、解释器等组件,不能在运行时解析和执行任何动态引入的类。

与传统 Java 运行模型相比,GraalVM 的静态编译运行模型有两大特点


一是静态编译后的可执行程序已经是本地程序,而且自包含了轻量级运行时支持,因此不再额外需要 Java 虚拟机。没有了 JVM,自然也就消除了图 1 中的响应时间无穷大阶段,使得应用程序达到即起即用的状态。另外,因为 JVM 的运行也需要消耗一部分内存,去掉 JVM 后应用程序的内存占用也大幅降低。


二是静态编译后的程序也经过了众多的编译优化,运行时不再需要经过解释执行和 JIT 编译,既避免了解释执行的低效,也避免了 JIT 编译的 CPU 开销,还解决了传统 Java 执行模型中无法充分预热,始终存在解释执行的问题,因此可以保证应用程序始终以稳定的性能执行,不会出现性能波动。


这两个基本特点解决了 Java 程序冷启动问题—JVM 初始化的开销和从解释执行到 JIT 编译执行的开销,因此静态编译后的 Java 程序可以获得极速启动的效果。


图 3 给出了 OpenJDK 和静态编译后的 Java 程序的性能对比示意,其中蓝色线条为 OpenJDK 的运行时性能变化情况,红色线条为社区版 GraalVM 静态编译后的程序运行时性能变化情况,可以看到经过社区版 GraalVM 的静态编译后的 Java 应用的性能稳定地处于 OpenJDK 的 C1 编译器的水平。而商业版的 GraalVM 静态编译甚至可以使程序达到 C2 编译器的编译后的性能水平。

4d4a2069163846b8bf3aeeedf6de105e.jpg

图 3 OpenJDK与GraalVM静态编译的Java程序性能对比示意图


由此可见:Java 静态编译技术能够彻底解决 Java 冷启动问题,使得 Java 语言在云原生应用的浪潮中继续保持强大的竞争力,可谓是 Java 语言的“大杀器”了。

相关文章
|
18天前
|
缓存 算法 Java
Java 实现的局域网管控软件的性能调优
局域网管控软件在企业网络管理中至关重要,但随着网络规模扩大和功能需求增加,其性能可能受影响。文章分析了数据处理效率低下、网络通信延迟和资源占用过高等性能瓶颈,并提出了使用缓存、优化算法、NIO库及合理管理线程池等调优措施,最终通过性能测试验证了优化效果,显著提升了软件性能。
30 1
|
3月前
|
Kubernetes Cloud Native Java
云原生之旅:从容器到微服务的演进之路Java 内存管理:垃圾收集器与性能调优
【8月更文挑战第30天】在数字化时代的浪潮中,企业如何乘风破浪?云原生技术提供了一个强有力的桨。本文将带你从容器技术的基石出发,探索微服务架构的奥秘,最终实现在云端自由翱翔的梦想。我们将一起见证代码如何转化为业务的翅膀,让你的应用在云海中高飞。
|
3月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
105 1
|
9天前
|
Java 数据库连接 数据库
优化之路:Java连接池技术助力数据库性能飞跃
在Java应用开发中,数据库操作常成为性能瓶颈。频繁的数据库连接建立和断开增加了系统开销,导致性能下降。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接,显著减少连接开销,提升系统性能。文章详细介绍了连接池的优势、选择标准、使用方法及优化策略,帮助开发者实现数据库性能的飞跃。
18 4
|
7天前
|
Java 数据库连接 数据库
深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能
在Java应用开发中,数据库操作常成为性能瓶颈。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能。文章介绍了连接池的优势、选择和使用方法,以及优化配置的技巧。
11 1
|
14天前
|
存储 缓存 算法
提高 Java 数组性能的方法
【10月更文挑战第19天】深入探讨了提高 Java 数组性能的多种方法。通过合理运用这些策略,我们可以在处理数组时获得更好的性能表现,提升程序的运行效率。
15 2
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
365 37
|
30天前
|
缓存 Java 数据库连接
使用 NCache 将 Java 微服务扩展到极致性能
使用 NCache 将 Java 微服务扩展到极致性能
26 8
|
2月前
|
缓存 Java 应用服务中间件
Java虚拟线程探究与性能解析
本文主要介绍了阿里云在Java-虚拟-线程任务中的新进展和技术细节。
|
2月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。