一些杂想:Java老矣,尚能饭否?
阿里妹导读
本文就Java真的老了吗展开讲述,诠释了作者作为一名Java开发者的所思所感。
“落寞”的Java
"Write Once, Run Everywhere"的平台无关特性在当年确实是真香,但现在这种部署的便利性已经完全可以交由Docker为代表的的容器提供了(从某种意义上说,JVM也是字节码的容器),而且做得更好,可以将整个运行环境进行打包。想想Docker的口号也是:"Build Once, Run Anywhere"。
Java 总体上是面向大规模、长时间运行的服务端应用而设计的。在语法层面,Java+Spring框架写出的代码一致性很高;在运行期,有JIT编译、GC等组件保障应用稳定可靠。这些特性对于企业级应用十分关键,曾经是Java最大的优势之一。但在微服务化甚至Serverless化的部署形态下,有了高可用的服务集群,也无须追求单个服务要 7×24 小时不可间断地运行,它们随时可以中断和更新,Java的这一优势无形中被削弱了。
另一个广为诟病的是Java的资源占用问题,这主要包含两方面:静态的程序大小和动态的内存占用。
不管多大的应用,都要随身带一个臃肿的JRE环境(这里先不讨论模块化改造),加上各种复杂的Jar包依赖,看了下我们团队的每个Java应用的容器镜像大小都轻松上G。
应用的运行期内存占用居高不下,这个是Java天生的缺陷,很难克服。
JDK的演进
Java 9:难产的模块化
不可忽视的改造成本
虽然提供了未命名模块和自动模块,Oracle也提供了迁移指南和工具[4]供参考,但改造的成本依旧很大,特别是梳理模块之间的依赖关系,较为繁琐。
小心使用内部API
模块化的最大卖点之一是强大的封装性,它确保非public类以及非导出包中的类无法从模块外部访问。但在这之前,jar包中类的访问是没有限制的(即使是private也可以通过反射访问)。比如JDK中的大部分com.sun.* 和 sun.*包是内部无法访问的,但这之前被用得很多(出于性能/向前兼容等等原因),虽然Oracle的建议是不要使用这些类:Why Developers Should Not Write Programs That Call 'sun' Packages[5]。
小心使用内部JAR
像lib/rt.jar和lib/tools.jar等内部 JAR不能再访问了。不过正常来说,应该只有IDE或类似工具会直接依赖?
小心使用JAR中的资源
一些API会在运行期获取JAR中的资源文件(例如通过ClassLoader.getSystemResource),在Java9之前会拿到 jar:file:<path-to-jar>!<path-to-file-in-jar>这类格式的URL Schema,而Java9之后则变成了 jrt:/<module-name>/<path-to-file-in-module>
其他一些问题[6]
我已经分成不同jar包了,我感觉这样就可以了,有必要更进一步吗? 我又不是开发中间件和框架的,我开发业务应用,为什么要关心这些? 就算我有二方包要开放出去,为二方包维护模块定义似乎也带不来多少收益? 该如何分离每个模块,基于什么原则?就跟DDD一样,我知道这东西很美好,有最佳实践可以参考吗?
compact strings[8],通过对底层存储的优化来减少String的内存占用。String对象往往是堆内存的大头(通常来说可以达到25%),compact string可以减少最多一倍的内存占用;
AOT编译[9],一个实验性的AOT编译工具jaotc[10]。它借助了Graal编译器,将所输入的Java类文件转换为机器码,并存放至生成的动态共享库之中。jaotc的一大应用便是编译java.base module(也就是模块化后Java核心类库中最为基础的类)。这些类很有可能会被应用程序所调用,但调用频率未必高到能够触发即时编译。
JVMCI[11]( JVM 编译器接口),另一个experimental的编译特性。用Java写Java编译器,Java也可以说我能自举了!
JVMCIJIT编译器与JVM的交互可以分为如下三个方面。
响应编译请求; 获取编译所需的元数据(如类、方法、字段)和反映程序执行状态的profile; 将生成的二进制码部署至代码缓存(code cache)里。 即时编译器通过这三个功能组成了一个响应编译请求、获取编译所需的数据,完成编译并部署的完整编译周期。 传统情况下,即时编译器是与Java虚拟机紧耦合的。也就是说,对即时编译器的更改需要重新编译整个Java虚拟机。这对于开发相对活跃的Graal来说显然是不可接受的。 为了让Java虚拟机与Graal解耦合,引入 JVMCI 将上述三个功能抽象成一个Java层面的接口。这样一来,在Graal所依赖的JVMCI版本不变的情况下,我们仅需要替换Graal编译器相关的jar包(Java 9以后的jmod文件),便可完成对Graal的升级。 其实JVMCI接口就长这样: public interface JVMCICompiler {
/**
* Services a compilation request. This object should compile the method to machine code and
* install it in the code cache if the compilation is successful.
*/
CompilationRequestResult compileMethod(CompilationRequest request);
}
Java 10:小升级
G1的多线程并发mark-sweep-compact:这个feature的背景是G1垃圾回收器在Java9中引入,但那会还使用单线程做mark-sweep-compact。
Application Class-Data Sharing[12]:通过在不同Java进程间共享应用类的元数据来降低启动时间和内存占用,算是对Java 5引入的CDS的扩展,在这之前只支持Bootstrap Classloader加载的系统类。
其实这个特性还挺有用的,因为Java启动慢很大一部分时间耗在类加载上,CDS生成的存档类似于一个快照,在运行时可以直接做内存映射,还可以在多个JVM之间共享存档文件来减少内存占用。这个JEP中也提了一嘴:对Serverless云服务的分析表明,其中许多在启动时加载了数千个应用程序类,AppCDS 可以让这些服务快速启动并提高整体系统响应时间。
Docker的支持[13]更好了,能认出Docker环境了。
Java 11:ZGC闪亮登场
Java 12:Shenandoah和内存返还
一方面,Java是一门有GC的语言,垃圾对象会持续占用内存,直到下一次GC为止 另一方面,GC算法也决定了更多的内存占用,例如:
CMS的做法是在老年代达到指定的占用率后(Java 6后默认为92%)开始GC,可以通过-XX:CMSInitiatingOccupancyFraction参数调高这个值,但调得太高又容易碰到Concurrent Mode Failure;
G1的解法则是为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上,并且默认不回收在这个地址以上的对象。
Java 13:小升级+1
ZGC的增强[20]:同G1和Shenandoah一样,可以将未使用的内存返还给操作系统了
AppCDS的增强[21]:在Java10的AppCDS基础上支持动态归档,可以在程序退出时自动创建
Java 14:小升级+2
ZGC支持Mac和Windows了(不过大部分生产环境应该不会用这俩?)
G1支持Numa-Aware的内存分配[22]:NUMA(Non-Uniform Memory Access,非统一内存访问架构)的介绍可以参考下这篇文章:【计算机体系结构】NUMA架构详解[23]。在NUMA架构下,G1收集器会优先尝试在请求线程当前所处的处理器的本地内存上分配对象,以保证高效内存访问。在G1之前的收集器就只有针对吞吐量设计的Parallel Scavenge支持NUMA内存分配,如今G1也成为另外一个选择。
Java 15:ZGC和Shenandoah转正
Java 16:Alipine Linux的支持
ZGC支持并发线程堆栈处理[24]
弹性元空间[25]:一般Java程序里元空间(metaspace)的内存占用相比起堆来说不算高,但也很容易出现出现内存浪费。Java 16优化了元空间的内存分配机制来减少内存占用。
Java 17:最新的LTS版本
Project X
Project Amber[30]:旨在探索和孵化更小的、以生产力为导向的 Java 语言功能,每个提案的特性都不大,很多已经落地到不同JDK版本中了,像是Records[31]、Sealed Class[32]、Pattern Matching、Text Blocks[33]等等。
Project Leyden[34]:旨在解决Java的启动时间、TTP(Time to Peak)性能、内存占用等顽疾。一个特性即是AOT编译,但难度太大,短期内指望不上,先寄希望于GraalVM。
Project Loom[35]:Java的协程和结构化并发[36]。
Project Valhalla[37]:旨在探索和孵化高级Java VM和语言特性,例如值类型(Value types)[38]和基于值类型的泛型[39]。
Project Portola[40]:将 OpenJDK 向 Alpine Linux 移植,在Java 16中已经得到了落地。
Project Panama[41]: 更好地跟本地代码(主要是C代码)交互。
Project Lilliput[42]:将对象头缩减到64bit来降低内存占用。
提前编译-AOT
启动慢,Java启动需要初始化虚拟机,加载大量的类
预热慢,在JIT编译器介入前,需要在解释模式下运行
Java是一门跨平台语言,但JVM并不是跨平台的,Java将源码编译成字节码,交给JVM执行,这中间装载的开销很高。
一段程序想要被加载需要经过的流程:
new 字节码或者 static 相关字节码触发类加载 从一系列 jar 包中找到感兴趣的 class 文件 将 class 文件的读取到内存里的 byte 数组 defineClass,包括了 class 文件的解析、校验、链接 类初始化(static 块,或者静态变量初始化) 开始解释执行 2000 次解释后被 client compiler JIT 编译,随后 15000 次执行后被 server compiler JIT 编译
峰值性能:AOT编译不像JIT编译一样能收集程序运行时的信息,因此也无法进行一些更激进的优化,例如基于类层次分析的完全虚方法内联,或者基于程序profile的投机性优化(不过这并非硬性限制,我们可以通过限制运行范围,或者利用上一次运行的程序profile来绕开这些限制)。
构建时长:从目前的实测数据看,像Graal编译器花的构建时间都比正常编译时间要长。不过这个也在情理之中,毕竟一个只需要把代码编译成字节码,一个则需要扫描然后分析程序所有的依赖做静态编译。
在生产的本地镜像(Native Image)中使用Java agents,JMX,JVMTI,JFR等组件会有一些限制。
(最关键的)动态特性的支持:AOT编译很美好,但是在Java中实现起来却很困难,主要的原因在于Java虽然是一门静态语言,但是也包含了很多动态特性,比如反射、动态代理、动态类加载、字节码Instrument (BCI) 等等,而提前编译要求满足封闭世界假设( closed world assumption),在编译期就确定程序用到的类。
这是一个很简单的取舍问题,因为动态特性在Java中用得实在是太普遍了,不管是Spring、Hibernate这些应用框架还是CGLib这类字节码生成库,大部分生产力工具都依赖这些动态特性,所以Java的提前编译至今还是Experimental状态。
目前来看使用AOT难免需要有一些折中,例如后面要讲到的Substrate VM就要求以配置的方式明确告知编译器程序代码中有哪些方法是只通过反射来访问的,哪些类会被动态加载等等。然而另一些功能可能只能妥协或者放弃了,就像动态生成字节码这类十分常用的功能,我们熟知的Spring默认就会使用CGLib生成动态代理。从 Spring Framework 5.2 开始增加了@proxyBeanMethods注解来排除对 CGLib 的依赖,仅使用标准的动态代理去增强类,但这也就限制了动态代理的能力。
协程(虚拟线程)
协程是协作式的,线程是抢占式;
协程在用户模式下,由应用程序调度管理,而线程则由操作系统内核管理;
(有栈)协程拥有自己的寄存器上下文和栈,但比线程要小得多(MB和KB级别的差距),切换也快得多;
一个线程可以包含一个或多个协程,即不同的协程可以在一个线程上被调度。协程也被称为轻量级线程,有意思的是线程有时候也被成为轻量级进程;
1:1的模型对于计算密集型任务这很合适,既不用自己去做调度,也利于一条线程跑满整个处理器核心;但对于 I/O 密集型任务,譬如访问磁盘、访问数据库占主要时间的任务,这种模型就显得成本高昂,主要在于内存消耗和上下文切换上:64 位 Linux 上 HotSpot 的线程栈容量默认是 1MB,线程的内核元数据(Kernel Metadata)还要额外消耗 2-16KB 内存,所以单个虚拟机的最大线程数量一般只会设置到 200 至 400 条,当程序员把数以百万计的请求往线程池里面灌时,系统即便能处理得过来,其中的切换损耗也是相当可观的。
在此之前,Java中已经有一些三方的实现支持协程,比如Quasar[49]和Coroutines[50],貌似都是需要挂载agent利用字节码注入的方式实现,我没有细看,有兴趣的可以了解下。
并发任务的数量很高(超过几千个)
工作负载不受 CPU 限制,换句话说是I/O密集型的任务。如果是计算密集型任务,拥有比处理器内核多得多的线程并不能提高吞吐量
举个例子,假设有这样一个场景,需要同时启动10000个任务做一些事情:
// 创建一个虚拟线程的Executor,该Executor每执行一个任务就会创建一个新的虚拟线程
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
doSomething();
return i;
});
});
} // executor.close() is called implicitly, and waits
把Executors.newVirtualThreadPerTaskExecutor()换成Executors.newCachedThreadPool()。结果是程序会崩溃,因为大多数操作系统和硬件不支持这种规模的线程数。 换成Executors.newFixedThreadPool(200)或者其他自定义的线程池,那这10000个任务将会共享200个线程,许多任务将按顺序运行而不是同时运行,并且程序需要很长时间才能完成。
如果doSomething()里执行的是某类计算任务,例如给一个大数组排序,那么虚拟线程还是平台线程都无济于事。JEP中提到了很关键的一点就是:虚拟线程不是更快的线程—它们运行代码的速度并不比平台线程快。它们的存在是为了提供scale(更高的吞吐量),而不是speed(更低的延迟)。
虚拟线程会保持原有统一线程模型的交互方式,通俗地说就是原有的 Thread、Executor、Future、ForkJoinPool 等多线程工具都应该能以同样的方式支持新的虚拟线程。使用虚拟线程的代码可能长这样:
// 直接创建一个虚拟线程
Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));
// 通过builder创建一个虚拟线程
Thread virtualThread = Thread.builder().virtual().task(() -> {
System.out.println("Fiber Thread: " + Thread.currentThread().getName());
}).start();
// 创建一个基于虚拟线程的ExecutorService
ExecutorService executor = Executors.newVirtualThreadExecutor()
虚拟线程既便宜又量大管饱,因此永远不应该被池化。大多数虚拟线程将是短暂的并且具有浅层调用栈,执行的任务像是单个 HTTP 客户端调用或单个 JDBC 查询这样的I/O操作。相比之下,线程是重量级且昂贵的,因此通常必须被池化。 JDK的虚拟线程调度会借助ForkJoinPool[52],以 FIFO 模式运行。
值类型
内存延迟与处理器执行性能之间的冯诺依曼瓶颈[54](Von Neumann Bottleneck)增加了100-2000倍(也就是说,如果以CPU算术计算的速度为基准看,读内存的速度没有变快反而更慢了);
指针的间接获取对性能的影响变得更大,因为对指针的解引用是昂贵的操作,尤其是当指针或它指向的对象不在处理器的缓存中时(没办法,只能读内存了);
Java通过对象标识符进行链式访问,与之相对的是集中访问模式,例如C/C++中的struct会将对象在内存中拍平。两者的关键区别在于,链式访问需要读多次内存才能命中,而集中访问一次就可以将相关数据全部取出。打个比方,类A中包含类B,类B中包含类C,从A->B->C,链式访问在最坏情况下要读3次内存;而集中访问只需要读一次。
final class Point {
final int x;
final int y;
}
值类型的内存布局可以像基础类型一样平坦紧凑,其他对象或数组在引用值类型时更简单;
同样也不需要object header了,可以省去内存占用和分配的开销;
甚至JVM可以在栈上直接分配值类型,而不必在堆上分配它们;
可以使用inline关键词定义一个值类型:
inline public class Point {
public int x;
public int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
可以有变量+方法
可以继承接口,例如Point可以从某个Shape接口继承而来
可以通过封装来隐藏内部实现
可以作为泛型使用,可以有泛型参数
队友的助攻
GraalVM
Graal - 用Java写的编译器,既可以作为 JIT 编译器取代C2在传统的OpenJDK JVM上运行,又可以当做AOT编译器使用。
Substrate VM - 是一个构建在Graal编译器之上的,支持AOT编译的运行框架。它的设计初衷是提供一个快速启动,低内存占用,以及能无缝衔接C代码(与JNI相比)的runtime,并能完美适配Truffle[59]语言实现。
Truffle - 即下图中的语言实现框架(Language Implementation Framework),用来支持多种语言跑在GraalVM上。
我们熟知的HotSpot有两个JIT编译器,C1和C2。Java 程序首先在解释模式下启动,执行一段时间后,经常被调用的方法会被识别出来,并使用 JIT 编译器进行编译——先是使用 C1,如果 HotSpot 检测到这些方法有更多的调用,就使用 C2 重新编译这些方法。这种策略被称为“分层编译”,是 HotSpot 默认采用的方式。经过这么多年优化下来,C2编译后的代码效率非常出色,可以与 C++ 相媲美(甚至更快)。不过,近年来 C2 并没有带来多少重大的改进。不仅如此,C2 中的代码变得越来越难以维护和扩展,新加入的工程师很难修改使用 C++ 特定方言编写的代码。
native image builder:使用Graal编译器做静态编译的工具,它处理应用程序的所有类和依赖项(包括来自JDK的部分),通过指针分析(Points-To Analysis)来确定在应用程序执行期间可以访问哪些类和方法,然后提前将可访问的代码和数据编译为特定操作系统和架构的可执行文件或者动态链接库。
SubstrateVM Runtime:一个特殊的精简过的VM Runtime,包括了deoptimizer、GC、线程调度等组件。因为已经做了AOT编译,比传统的Runtime少了类加载、解释器、JIT等组件。
官网放了一张图来展示Graal Native Image的两大优势:快速启动和低内存占用。不过我看到的其他一些资料上说在低时延和高吞吐(Latency/Throughput)场景下并不占优。
动态类加载:对于像Class.forName("myClass”)一类动态按照类名加载的操作,必须在配置文件里配上myClass,否则运行期就是一个ClassNotFoundException;
反射:构建时会通过检测对反射 API 的调用做静态分析,对于无法通过静态分析获知的,那也只能配置了;
动态代理:这里指的是使用了java.lang.reflect.Proxy API的动态代理。要求动态代理的接口列表在构建期就是已知的,构建时会简单地拦截对java.lang.reflect.Proxy.newProxyInstance(ClassLoader, Class<?>[], InvocationHandler)和java.lang.reflect.Proxy.getProxyClass(ClassLoader, Class<?>[])的调用来确定接口列表。同样,如果分析失败,那也只能配置了;
JNI:本机代码可以按名称访问 Java 对象、类、方法和字段,其方式类似于在 Java 代码中使用反射 API。一种替代的方式是可以考虑使用GraalVM提供的原生接口org.graalvm.nativeimage.c[65],更简单开销更低,缺点是不允许从 C 代码访问 Java 数据结构;
序列化:Java 序列化需要类的元数据信息才能起作用,因此也需要提前配置(不过,你的代码里还在用 Java 序列化吗?);
还有一些限制条件,像是invokedynamic字节码和Security Manager,是直接无法兼容的。还有一些功能跟HotSpot有区别,具体可以参考这篇文档[66]。
Truffle
完整的列表参考这里[72]。
const express = require('express')
const app = express()
const BigInteger = Java.type('java.math.BigInteger')
app.get('/', function (req, res) {
var text = 'Hello World from Graal.js!<br> '
// Using Java standard library classes
text += BigInteger.valueOf(10).pow(100)
.add(BigInteger.valueOf(43)).toString() + '<br>'
// Using R methods to return arrays
text += Polyglot.eval('R',
'ifelse(1 > 2, "no", paste(1:42, c="|"))') + '<br>'
// Using R interoperability to create graphs
text += Polyglot.eval('R',
`svg();
require(lattice);
x <- 1:100
y <- sin(x/10)
z <- cos(x^1.3/(runif(1)*5+10))
print(cloud(x~y*z, main="cloud plot"))
grDevices:::svg.off()
`);
res.send(text)
})
app.listen(3001, function () {
console.log('Example app listening on port 3001!')
})
关于spring-native,ATA上已经有大佬们做过比较深入的分析了,比如:让Spring启动提速95.5倍,项目解读之Spring-Graalvm-Native,也可以参考下官方的announcing-spring-native-beta[75]。
其他:Quarkus/Micronut/Helidon等等
Cloud Native Container First GraalVM Reactive Fast Boot And Low Memory Footprint
未来?
更具生产力的语法和API改进 以ZGC为代表的更先进的GC 在启动速度、内存占用等短板上的各种优化 以GraalVM为代表的新编译器+Native Image+多语言编程 更好的云原生支持
参考链接:
[1]https://openjdk.java.net/
[2]https://www.oracle.com/corporate/features/understanding-java-9-modules.html
[3]https://openjdk.java.net/projects/jigsaw
[4]https://docs.oracle.com/javase/9/migrate
[5]https://www.oracle.com/java/technologies/faq-sun-packages.html
[6]https://nipafx.dev/java-9-migration-guide
[7]https://www.reddit.com/r/java/comments/djycls/is_anyone_actually_using_modules_jigsaw
[8]https://openjdk.java.net/jeps/254
[9]http://openjdk.java.net/jeps/295
[10]http://openjdk.java.net/jeps/295
[11]http://openjdk.java.net/jeps/243
[12]https://openjdk.java.net/jeps/310
[13]https://www.docker.com/blog/improved-docker-container-integration-with-java-10
[14]https://openjdk.java.net/jeps/333
[15]https://www.zhihu.com/question/356585590
[16]https://blogs.oracle.com/javamagazine/post/understanding-the-jdks-new-superfast-garbage-collectors
[17]https://openjdk.java.net/jeps/318
[18]https://openjdk.java.net/jeps/189
[19]https://openjdk.java.net/jeps/346
[20]https://openjdk.java.net/jeps/351
[21]https://openjdk.java.net/jeps/350
[22]https://openjdk.java.net/jeps/345
[23]https://houmin.cc/posts/b893097a
[24]https://openjdk.java.net/jeps/376
[25]https://openjdk.java.net/jeps/387
[26]https://openjdk.java.net/jeps/386
[27]https://alpinelinux.org
[28]https://en.wikipedia.org/wiki/Musl
[29]https://openjdk.java.net/projects
[30]https://openjdk.java.net/projects/amber
[31]https://openjdk.java.net/jeps/395
[32]https://openjdk.java.net/jeps/409
[33]https://openjdk.java.net/jeps/378
[34]https://mail.openjdk.java.net/pipermail/discuss/2020-April/005429.html
[35]https://wiki.openjdk.java.net/display/loom/Main
[36]https://openjdk.java.net/jeps/8277129
[37]https://openjdk.java.net/projects/valhalla
[38]https://openjdk.java.net/jeps/169
[39]https://openjdk.java.net/jeps/218
[40]https://openjdk.java.net/projects/portola
[41]https://openjdk.java.net/projects/panama
[42]https://wiki.openjdk.java.net/display/lilliput
[43]https://www.infoq.cn/article/rqfww2r2zpyqiolc1wbe
[44]https://zh.wikipedia.org/wiki/协程
[45]https://en.wikipedia.org/wiki/Thread_(computing)
[46]https://en.wikipedia.org/wiki/Thread_(computing)
[47]https://openjdk.java.net/jeps/425
[48]https://mthli.xyz/stackful-stackless
[49]https://github.com/puniverse/quasar
[50]https://github.com/offbynull/coroutines
[51]https://openjdk.java.net/jeps/425
[52]https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/concurrent/ForkJoinPool.html
[53]https://www.youtube.com/watch
[54]https://en.wikipedia.org/wiki/Von_Neumann_architecture
[55]https://en.wikipedia.org/wiki/Identity_(object-oriented_programming)
[56]https://openjdk.java.net/jeps/218
[57]https://www.graalvm.org
[58]http://openjdk.java.net/jeps/317
[59]https://github.com/graalvm/graal/tree/master/truffle
[60]https://www.graalvm.org
[61]https://martijndwars.nl/2020/02/24/graal-vs-c2.html
[62]https://www.youtube.com/watch
[63]https://www.graalvm.org/native-image
[64]https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/BuildConfiguration.md
[65]http://www.graalvm.org/sdk/javadoc
[66]https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/Limitations.md
[67]https://github.com/graalvm/graaljs
[68]https://github.com/oracle/truffleruby
[69]https://github.com/oracle/fastr
[70]https://github.com/graalvm/graalpython
[71]https://github.com/oracle/graal/tree/master/sulong
[72]https://www.graalvm.org/22.0/graalvm-as-a-platform/language-implementation-framework/Languages
[73]https://github.com/spring-projects-experimental/spring-native
[74]https://github.com/spring-projects-experimental/spring-fu
[75]https://spring.io/blog/2021/03/11/announcing-spring-native-beta
[76]https://quarkus.io
[77]https://micronaut.io
[78]https://helidon.io
微信扫码关注该文公众号作者