下一代的JDK - GraalVM

news2024/9/20 7:53:38

GraalVM是最近几年Java相关的新技术领域不多的亮点之一, 被称之为革命性的下一代JDK,那么它究竟有什么神奇之处,又为当前的Java开发带来了一些什么样的改变呢,让我们来详细了解下

下一代的JDK

官网对GraalVM的介绍是 “GraalVM 是一个高性能 JDK,可提高基于 Java 和 JVM 的应用的性能并简化 Java 云原生服务的构建和运行。它提供优化的编译器,可以更快地生成代码并降低计算资源消耗,实现微服务即时启动“

这里面有几个关键词:高性能云原生低资源即时启动。而这几个关键词其实恰恰也是当前Java应用开发面临的困境。大家都知道当前不管是企业服务还是互联网领域都在强调云原生开发,但云原生的一个核心要素就是需要资源和服务的快速调度,毕竟服务器和带宽都是要钱的,用户只想在使用服务时付费,而不是长时间让应用空转。比如目前流行的serverless模式,要求函数的生命周期尽可能的短,意味着频繁的启动和销毁,这对于服务的启动性能,资源消耗,服务包大小都有比较高的要求。但对于当前的JAVA应用来说却是无可承受的痛。所以为啥现在go,rust这些语言大行其道,其实就是迎合了云原生的需求场景。Java作为一个拥有20多年历史的语言,当然不可能坐以待毙,所以也在不断的探索怎么提升启动时间,减少资源消耗,GraalVM应该算是目前这些探索当中最靠谱,最成熟的一个。

如果GraalVM真的能解决以上的问题,那称其为革命性的下一代JDK或者说是云原生时代Java的希望之星也不为过吧。目前包括Spring Boot, Micronaut, Quarkus 这些知名框架也都对GraalVM提供了原生的支持。

那GraalVM究竟是如何做到这一切的呢?我们先来看一下GraalVM的架构图
在这里插入图片描述

从图上可以看出来,其实GraalVM核心是由两个部分构成的。其中之一是一个全新的名为Graal的编译器,它是完全用Java语言编写的,就是图中红色的部分,这也是GraalVM的名字的来源,可想而知这个编译器的重要性。

另外一个重要的组成部分就是用于构建高性能动态语言解释器的框架叫做 Truffle,就是最上面的这一层,它也是GraalVM能够支持多种编程语言运行在其上的基础框架,它现在支持的语言有很多呀,包括pyhton,js这些常见的脚本语言,借助于LLVM编译器,甚至可以让C或者C++都跑在GraalVM之上,这在以前是很难想象的。

最底层就是我们非常熟悉的 Hotspot JVM,这部分GraalVM 并没有对Hotspot JVM做大的调整,包括垃圾回收器,类加载器,线程管理,诊断和监控等特性都是直接复用Hotspot JVM的功能,这也保证了GraalVM能够与现有的JAVA生态保持完全的兼容,只是专注于编译优化和性能提升

Graal编译器

Graal实际上是一个用Java语言编写的JIT编译器。它的主要作用就是替换原本Hotspot JVM中用C++写的另外两个JIT编译器:C1和C2。C1主要是用client模式下,我们应该接触的比较少,C2是Hotspot JVM自带的高性能JIT编译器,当年Hotspot JVM出现时,正是凭借基于热点探测的JIT技术在性能上吊打其它的一众JVM,成为JAVA领域这么多年来事实的标准,它的名字(HOTSPOT)也正好反应了它的这个重要特性。

为啥还需要另一个JIT编译器

既然有了C2这样强大的JIT编译器,为啥还要重新开发一个呢?而且还是用Java语言来开发,Java的性能能比C++高吗?其实重写这个编译器还真不是为了优化性能,C2的性能已经非常好了,问题的关键在于C2是C++写的,而且还写的很复杂,年代也很久远,这让C2项目变得非常难以维护和扩展。

我们来看看来自C2编译器背后大佬Cliff Click的抱怨:

在这里插入图片描述
在这里插入图片描述

于是用JAVA这样一种更容易维护,安全性也要比C++高很多的语言来重写编译器也就不是 一件不可想象的事情了。至于性能,Graal毕竟比C2晚了快20年,后发优势是非常明显的,可以做到一些原来在C2中很难做到的编译优化,比如部分逃逸分析,经过测试目前Graal编译器的峰值性能已经追平甚至部分超越了C2。

另外,为了让类似Graal这样的编译器也能够有效的和底层的Hotspot VM配合工作,JAVA社区还提出了一个名为JVMCI(JAVA虚拟机编译器接口) 的提案,本质上就是将JIT编译器和底层的虚拟机解耦,提供更强的扩展性。

Java的自举

用Java编写Java的编译器,这也带来了一个里程碑式的变化,Java终于能够自举了!我们来撸一撸其中的过程:

在这里插入图片描述

首先,我们用java写的源代码会被javac工具编译成平台无关的字节码,这个javac工具就是完全用java语言实现的。然后字节码在运行时通过jvm的 解释器或者JIT 编译器编译成本地代码,这个解释器和JIT编译器也是完全用java语言编写的,这样就完成了闭环,这使得JAVA能够完全摆脱对其它语言的依赖,从而成为一门完全自主的语言。

静态编译

前面的Graal编译器虽然意义重大,但依然只是一个JIT编译器,Java应用的编译执行方式和原来也没有多大的区别,如果仅仅是这样那显然GraalVM也不会有这么大的影响力。我们再来看一个真正可称之为GraalVM的杀手锏的功能—静态编译

JIT与AOT

我们都知道,Java为了实现其平台无关性,首次编译(javac)的产出物其实是平台无关的字节码,这些字节码是无法直接被执行的。最早的Java Runtime都是通过JVM内置的解释器直接将字节码即时转换成本地代码来执行的,这样解释执行的方式肯定慢呀,所以当HOTSPOT JVM首次引入了JIT—即时编译或者叫做动态编译这样的概念以后,JAVA才真正确定了自己在业界的地位,也让HOTSPOT JVM一直作为官方虚拟机延续到了现在。

JIT(Just-in-Time) 简单的说,就是在运行时,把一部分热点代码,就是反复会调用的方法直接编译成本地代码,从而提升程序的运行效率。大家可能会有这样的疑问,为啥只编译热点代码呢,全部都编译成本地代码不是更快吗?因为编译很慢呀,开销也很大,如果编译消耗的资源都大于整个程序消耗的资源了,那么这么做就是本末倒置了。所以JIT就是一种折中的做法,把频繁运行的方法编译成本地代码提升执行效率,其它偶尔才运行的代码还是通过解释器去执行,这样能够最大程度的平衡启动时间,资源消耗和性能。

– 与之相对的就是静态编译,也叫做AOT(Ahead-of-Time) 编译。这个名词最早被大家听说应该是在android领域,在安卓5.0版本,当时谷歌引入了ART的新的运行时环境,并推出了AOT编译模式,故名思意AOT编译是在应用安装时一次性把字节码编译成机器码,而不是像JIT那样切香肠式的编译,这样能大幅提升应用的启动速度。但早期的AOT编译会造成应用安装时间过长,空间占用较多的问题,后来谷歌又改成AOT和JIT混合编译的策略了。说回JAVA哈,安卓和JAVA本就是同源的,安卓可以AOT编译,JAVA当然也可以,最早在JAVA 9.0 , 就引入了这方面的概念和支持,这个版本有一个AOT编译器叫做jaotc(对应原来的javac),但因为各种原因一直没得到多少重视。

我们可以通过一张图来比较一下两种编译方式各自的特点:

在这里插入图片描述

一般来说,AOT在启动速度,内存占用,包体积等方面占优势,但编译过程较慢;而JIT编译很快,二期因为可以基于运行时的信息做更激进的编译优化,因此在峰值吞吐量和响应延迟这些方面可能会更有优势。

Native Image

AOT和JIT应该说是各有所长,不存在谁取代谁的问题。但在云原生,微服务架构大行其道的当下,大家更看重的还是应用的启动速度和资源占用等特点,所以静态编译又变成了一个非常有诱惑力的选型。GraalVM也提供了自己的java静态编译工具,也就是 Native Image 。这个工具可以直接将Java应用程序编译成可本地执行的文件,脱离虚拟机直接运行。它背后需要解决三方面的问题:

编什么

源代码肯定要编译,但是依赖呢?是不是所有依赖都要编译。理论上所有依赖都编译肯定是没有问题的,而且后面反射的问题也顺带就解决了。但现实场景这基本不可能,我们现在的应用程序依赖链一般都很深,如果把classpath上能扫描到的jar包都给编译了,那估计真的要等到天荒地老了。所以这一步是走不通的。

Native Image采用的方法是构建一个从应用程序的入口函数,也就是main方法开始,到所有可达代码的这样一个调用图。这个调用图可以认为是应用程序在实际执行时所有可能的执行路径之和,只要构造了这样的图,也就知道了哪些方法最终是可能被执行的,这些方法也就需要被编译。

在这里插入图片描述

如何适配JVM运行时

JVM运行时可不单是提供JIT的支持,垃圾回收,反射,动态代理这些所谓的运行时特性也需要在编译成静态本地代码时解决。这个的解决方案有点简单粗暴,就是直接在编译后的执行文件中塞一个精简后的JVM— SVM(Substrate VM) ,它包括了一组轻量级的运行时库和基础设施,提供包括内存管理、垃圾回收、线程管理等一系列基础特性。

除了这些,还要解决反射调用,JNI调用这类动态调用的问题。刚才也提到过,Native Image编译的时候会做静态分析,从主函数入口开始遍历所有的可达的方法作为整个编译范围,但是这样的扫描有一个缺陷,就是无法确定那些只有在运行时才会获取到的动态调用信息,也就不会将动态调用的目标加入到编译范围,这样在运行时肯定就报错了。而且现在诸如spring这样的框架,大量使用了反射调用,如果置之不理,那么常规的应用直接编译肯定都无法正常使用的。

Native Image的解决方式是,开发者主动以配置文件的方式告诉编译器哪些方法是动态调用,这样编译器就能提前将其加入到编译范围中,避免运行时出现问题。当然这个配置文件手搓肯定不现实,就算自己写的代码可以统计,那么第三方库里面这么多动态调用也是无法统计的。所以还有一个配套的小工具:native-image-agent,应用程序可以先以jar包的方式进行预执行,并配置这个代理。这个代理程序会收集应用程序在执行期间所触发的所有动态调用信息,并自动生成配置文件。

$JAVA_HOME/bin/java -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/,config-write-period-secs=300,config-write-initial-delay-secs=5 ...

执行之后就会生成类似下面这样的配置文件:

在这里插入图片描述

有了这个配置文件以后,Native Image就会在编译时将反射的目标对象也加入到编译范围当中,并同时填充ReflectionData结构中,这个ReflectionData可以简单理解为反射对象的数据缓存,如果是在JVM中执行反射,第一次会先执行一次JNI调用,通过本地方法获取反射目标,再加入到ReflectionData缓存中,后续调用直接从缓存中读取目标对象信息,所以首次调用的开销要远高于后续的调用。而SVM在静态编译时就可以根据配置信息填充这个ReflectionData,这样反射调用就会比在JVM中要高效的多,这也是静态编译的一个优势。最后就是执行阶段,应用只需要简单的查询ReflectionData是否有相关目标信息,没有就直接报错,有才会调用。 整个反射的实现原理大概就是这样:

在这里插入图片描述
在这里插入图片描述

怎么编

最后再来看一下如何进行编译。编译器其实还是之前提到过的那个Graal编译器,但静态编译和JIT编译相比还是存在一些局限性,影响最大的就是无法获取运行时信息进行优化,比如如果基于运行时数据,JIT编译器发现某个IF判断几乎只执行其中一个分支,而另一个分支很少执行,那么JIT编译器在生成机器指令的时候就可以在IF判断前就准备好其中高频分支的相关数据,让后续代码能够快速执行,这就是所谓的分支预测。但静态编译因为不存在运行时数据,所以也就做不了相应的优化了,这里我列了一些静态编译和动态编译在编译期优化时的区别:

在这里插入图片描述

总结

GraalVM提供了Java应用程序运行的另一种方式,这将带来深远的影响,在云原生时代JAVA语言也终于有了自己的一席之地了。它主要有以下几个方面的好处:

  • 即时启动: 这个效果非常明显,基本就是所谓的秒起,和原来动辄等待十几二十秒简直是天壤之别。

  • 节约内存 : 经过实测空载内存差不多能够减少到原来的五分之一左右,虽然仍然比同一规模的go,rust等应用占用内存要多,但已经很惊艳了。

  • 安全性 : 这个可能是很多同学没想到的,这要从两个方面来分析:首先是静态编译仅编译实际应用需要的类和方法,这也意味着你即使引用了一个可能有安全漏洞的包,只要你实际运行的代码里面没有调用它有漏洞的方法,那就不会被攻击,相当于减少了被攻击的几率。另外Java之前反编译的风险是非常大的,因为编译产物是与源码结构类似的字节码,除非用一些比较复杂的方式来做编译期混淆,例如我们现在用的三方混淆插件,有多难用大家应该深有体会了。而静态编译后生成的是本地代码,不说完全解决了反编译的风险,至少安全性也是大大的提升了

但是静态编译如果要大规模应用到生产环境,目前还是存在一些局限性的,主要有下面一些:

  • 编译繁琐 GraalVM支持反射需要依赖于独立的配置,目前虽然可以通过native-image-agent的方式来自动生成配置文件,但这个依赖于预执行的路径覆盖,如果不能在预执行阶段做到覆盖主要的执行分支,那么有可能生成出来的反射配置文件也是不完整的。这个建议如果要在产品中使用的话,一定要为代码编写对应的单元测试用例,并尽可能覆盖到主要的业务分支,便于agent收集运行时数据进行编译。

  • 调试工具的缺乏 编译成可执行文件之后,原来JVM配套的那些调试和监控工具可能就用不了了,包括我们熟知的jstack,jmap,不过Graalvm也提供了一些替代的方式帮助我们对native应用进行观测和调试,这部分大家可以下来自己看一下,这里就不展开了

  • GC策略支持有限 目前版本的GraalVM仅支持Serial GC和G1两种垃圾回收器,而且社区版本还不支持G1,只有企业版能用。Serial GC就是串行回收,是Java比较早期的垃圾回收策略,虽然GraalVM中对其进行了优化,但是也仅推荐用于堆比较小的应用程序,不太适合大型应用。这个只能寄希望于Oracle后续能够将G1也下放给社区版了,大家显然也不太可能去购买企业版本。不过好消息是Oracle前不久刚刚出了一个GraalVM的闭源免费版本(Oracle GraalVM 区别于开源版本的 GraalVM CE ),特性完全和企业版是一样的,免费使用,只是不开源,有兴趣的同学可以去研究下它的license。

在这里插入图片描述
欢迎关注我的公号—飞空之羽的技术手札,有深度的技术好文~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1878102.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Python机器学习】模型评估与改进——交叉验证

交叉验证是一种评估泛化性能的统计学方法,它比单次划分训练集和测试集的方法更稳定、前面。在交叉验证中,数据被多次划分,并且需要训练多个模型。最常用的交叉验证是k折交叉验证,其中k是由用户指定的数字,通常取5或10,…

MySQL高级-InnoDB引擎-事务日志- redo log(事务持久性的保证)

文章目录 1、redo log1.1、重做日志缓冲(redo log buffer)1.2、重做日志文件(redo log file) 2、如果没有redo log,可能会存在什么问题的?2.2、我们一起来分析一下。 2.2、那么,如何解决上述的问…

240629_昇思学习打卡-Day11-Vision Transformer中的self-Attention

240629_昇思学习打卡-Day11-Transformer中的self-Attention 根据昇思课程顺序来看呢,今儿应该看Vision Transformer图像分类这里了,但是大概看了一下官方api,发现我还是太笨了,看不太明白。正巧昨天学SSD的时候不是参考了太阳花的…

Databend db-archiver 数据归档压测报告

Databend db-archiver 数据归档压测报告 背景准备工作Create target databend table启动 small warehouse准备北京区阿里云 ECSdb-archiver 的配置文件准备一亿条源表数据开始压测 背景 本次压测目标为使用 db-archiver 从 MySQL 归档数据到 Databend Cloud, 归档的…

qt 开发笔记 动态链接库应用

1.概要 1.1 需求 库有两种,动态库和静态库,这里说的是动态库;动态库的加载方式有两种,一直是静态的一种是动态的,这里的静态加载是指静态加载动态,是一种加载动态库的方式。也有一种动态加载的方式&#…

衣服、帽子、鞋子相关深度学习数据集大合集(1)

最近收集了一大波关于衣物深度学习数据集,主要有衣服、帽子、鞋子、短裤、短袖、T恤等。 1、运动裤、短裤图片数据集 数据格式:图片 是否标注:已标注 标注格式:yolov8 图片数量:915张 查看地址:https…

# Sharding-JDBC从入门到精通(2)- Sharding-JDBC 介绍

Sharding-JDBC从入门到精通(2)- Sharding-JDBC 介绍 一、概述-分库分表所带来的问题 1、分库分表带来的问题 分库分表能有效的缓解了单机和单库带来的性能瓶颈和压力,突破网络 IO、硬件资源、连接数的瓶颈,同时也带来了一些问题…

容器进程

一、容器进程和宿主机进程的关系 容器在进程空间上和宿主机是隔离的,每创建一个容器,该容器都有一个独属的进程空间简称PID NameSpace。但是容器本质也是一个进程,自然是由其父进程创建的,这个可以使用ps aux命令验证。 | 容器视…

Thinger.io 支持多协议、插件化100%开源 IoT 企业级物联网平台

项目源码,文末联系小编 Thinger.io 是一个开源插件化物联网平台,提供了设备原型、扩展和设备连接管理所需的一切工具。我们的目标是使物联网的使用民主化,使其可供全世界使用,并简化大型物联网项目的开发。 01 Thinger.io 物联网平…

【C++】哈希表 --- 闭散列版本的实现

在无人问津日子里 正是登峰造极的好时机 ——《人民日报》 哈希表 --- 闭散列版本的实现 1 C中的哈希表2 哈希表底层2.1 功能2.1 哈希冲突2.3 开散列与闭散列 3 闭散列版本的实现3.1 框架搭建3.2 仿函数设计3.3 插入函数3.4 查找函数3.5 删除函数 Thanks♪(・ω&a…

windows 10 安装tcping 使用教程

1 官网下载:tcping下载 2 复制tcping 到win10系统目录C:\Windows\System32 3 tcping 网址测试,可以指定端口 4 tcping 测试端口联通 5 tcping http模式

LeetCode 算法: 合并 K 个升序链表 c++

原题链接🔗:合并 K 个升序链表 难度:困难⭐️⭐️⭐️ 题目 给你一个链表数组,每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中,返回合并后的链表。 示例 1: 输入:lists […

Hugging Face Accelerate 两个后端的故事:FSDP 与 DeepSpeed

社区中有两个流行的零冗余优化器 (Zero Redundancy Optimizer,ZeRO)算法实现,一个来自DeepSpeed,另一个来自PyTorch。Hugging FaceAccelerate对这两者都进行了集成并通过接口暴露出来,以供最终用户在训练/微调模型时自主选择其中之…

Python | Leetcode Python题解之第191题位1的个数

题目: 题解: class Solution:def hammingWeight(self, n: int) -> int:ret 0while n:n & n - 1ret 1return ret

PAE:从潮流报告中提炼有效产品属性

本文将介绍PAE,一种用于包含 PDF格式的文本和图像的产品属性提取算法。目前大部分的方法侧重于从标题或产品描述中提取属性,或利用现有产品图像中的视觉信息。与之前的工作相比,PAE从潮流趋势报告的PDF文件中提取属性,提取的属性包…

ISO26262标准

什么是ISO26262? ISO 26262(国际功能安全标准)是一个涵盖整个汽车产品开发过程的汽车功能安全标准。ISO 26262继承或改编自工业自动化行业的安全要求标准IEC61508,但专门为汽车行业量身定制。最新版本是ISO26262-1:2018。 它包括诸如需求分析、安全分析…

一个简单的文件上传功能

代码如下&#xff1a; PostMapping("/upload")public ResponseEntity<String> handleFileUpload(RequestParam(value "uploadDirectory") String uploadDirectory,RequestParam("fileName") MultipartFile fileName) {try {// 确保文件不…

乱扔垃圾自动识别摄像头

如今&#xff0c;随着城市化进程的加快和人们生活水平的提高&#xff0c;环境保护和城市美观成为社会关注的焦点。乱扔垃圾问题长期困扰着城市管理者和居民&#xff0c;给城市环境卫生带来严重挑战。为了有效解决这一问题&#xff0c;乱扔垃圾自动识别摄像头应运而生&#xff0…

nvm-desktop window安装,支持动态切换nodejs版本

一、安装 nvm-desktop 概述 1 、卸载干净笔记的nodejs 和nodejs的环境变量 2、安装 nvm-desktop 软件 3、配置环境变量 4、测试功能 # 此时已安装完成 其他&#xff1a;常见nodejs的问题解决参考&#xff1a;官网 mac 安装教程 https://github.com/1111mp/nvm-desktop/blob/…

大模型微调实战之基于星火大模型的群聊对话分角色要素提取挑战赛:Task01:跑通Baseline

目录 0 背景1 环境配置1.1 下载包1.2 配置密钥1.3 测试模型 2 解决问题2.1 获取数据2.2 设计Prompt2.2 设计处理函数2.3 开始提取 附全流程代码 0 背景 Datawhale AI夏令营第二期开始啦&#xff0c;去年有幸参与过第一期&#xff0c;收获很多&#xff0c;这次也立马参与了第二…