JDK24的发布时间
时间 | 状态 |
2024/12/05 | Rampdown Phase One (branch from main line) |
2025/01/16 | Rampdown Phase Two |
2025/02/06 | Initial Release Candidate |
2025/02/20 | Final Release Candidate |
2025/03/18 | General Availability |
JDK24将于2025/03/18发布,其中特性之一的JEP 483(Ahead-of-Time Class Loading & Linking)也是Project Leyden的一部分,旨在通过AOT加载和初始化类来缩短应用的启动时间。
什么是AOT?
AOT(Ahead-of-Time)是一种编译技术,它将Java代码编译为机器代码,而不是在运行时进行JIT(Just-In-Time)解释执行。AOT编译器在应用程序启动之前将Java代码编译为机器代码,这样当 HotSpot Java 虚拟机启动时,让应用程序的类以加载和链接的状态立即可用,从而缩短启动时间。 要做到这一点,可在一次运行过程中监控应用程序,并将所有类的加载和链接形式存储在缓存中,供后续运行使用。 为今后改进启动和预热时间奠定基础。
什么是Project Leyden?
Leyden项目的主要目标是改进Java程序的启动时间、达到峰值性能的时间以及降低内存占用。一共包含三个JEP:
- JEP 483: Ahead-of-Time Class Loading & Linking
- JEP draft: Ahead-of-Time Code Compilation
- JEP draft: Ahead-of-Time Method Profiling
而JEP 483这个特性目前在jdk24的版本中已经处于Closed?/?Delivered的状态,并将随JDK24的发布而可以使用。
JEP 483的目标
- 利用大多数应用程序每次启动方式大致相同这一特点来缩短启动时间。
- 无需对应用程序、程序库或框架的代码进行任何修改。
- 除了与此功能直接相关的命令行选项外,不会更改从命令行启动java应用程序的方式。
- 不需要使用 jlink 或 jpackage 工具。
- 为持续改进启动时间和预热时间(即 HotSpot JVM 为达到最高性能而优化应用程序代码所需的时间)奠定基础。
- 目前使用AOT来提前缓存由用户定义的类加载器加载的类还不受支持,只有通过JDK内置类加载器从类路径、模块路径和 JDK本身加载的类才能被缓存。可能会在以后解决这一限制。
原理
通过对HotSpot JVM进行扩展,使其支持AOT缓存,在读取、解析、加载和链接类之后将其缓存存储起来。一旦为特定应用程序创建了缓存,就可以在该应用程序的后续运行中重复使用,以缩短启动时间。
如何创建AOT缓存
安装JDK24
下载OpenJDK JDK 24 Early-Access Builds,并解压到指定目录
curl -O https://download.java.net/java/early_access/jdk24/33/GPL/openjdk-24-ea+33_linux-x64_bin.tar.gz
tar -xvf openjdk-24-ea+33_linux-x64_bin.tar.gz
vim ~/.zshrc
export JAVA_HOME=~/software/jdk-24
export PATH=$JAVA_HOME/bin:$PATH
source ~/.zshrc
新建一个Java的测试文件
vim ComplexJDKClassLoaderExample.java
import java.io.IOException;
import java.lang.reflect.*;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.time.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public class ComplexJDKClassLoaderExample {
public static void main(String[] args) throws Exception {
// --------------------------
// 1. 多线程与并发工具类
// --------------------------
ExecutorService executor = Executors.newFixedThreadPool(4);
List> futures = new ArrayList<>();
// 提交多个并发任务
for (int i = 0; i < 10; i++) {
futures.add(executor.submit(() -> {
// 使用 ConcurrentHashMap 和原子操作
ConcurrentHashMap map = new ConcurrentHashMap<>();
map.computeIfAbsent(1, k -> "Value-" + ThreadLocalRandom.current().nextInt());
// 时间处理
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC"));
Duration duration = Duration.between(now, now.plusHours(1));
}));
}
// --------------------------
// 2. NIO 文件操作
// --------------------------
Path tempFile = Files.createTempFile("nio-example", ".txt");
Files.write(tempFile, "Hello NIO!".getBytes(), StandardOpenOption.WRITE);
Files.readAllLines(tempFile).forEach(System.out::println);
// --------------------------
// 3. 反射与动态代理
// --------------------------
Runnable proxyInstance = (Runnable) Proxy.newProxyInstance(
ComplexJDKClassLoaderExample.class.getClassLoader(),
new Class[] { Runnable.class },
(proxy, method, args1) -> {
System.out.println("Dynamic proxy invoked!");
return null;
});
proxyInstance.run();
// --------------------------
// 4. HTTP 客户端请求
// --------------------------
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get"))
.GET()
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP Status Code: " + response.statusCode());
// --------------------------
// 5. 复杂集合与流操作
// --------------------------
List>>> nestedCollection = new ArrayList<>();
IntStream.range(0, 100)
.parallel()
.mapToObj(i -> {
Map> map = new TreeMap<>();
map.put(i, Collections.singleton("Item-" + i));
return map;
})
.collect(Collectors.groupingByConcurrent(map -> map.keySet().hashCode() % 10))
.values()
.forEach(nestedCollection::add);
// --------------------------
// 6. 强制类初始化(触发更多类加载)
// --------------------------
Class.forName("java.security.SecureRandom", true, ClassLoader.getSystemClassLoader());
Class.forName("javax.crypto.Cipher", true, ClassLoader.getSystemClassLoader());
// 等待所有线程完成
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
}
}
编译Java文件并创建jar包
javac ComplexJDKClassLoaderExample.java
jar --create --verbose --file app.jar --main-class ComplexJDKClassLoaderExample *.class
运行一次应用程序以记录其AOT配置到app.aotconf文件中
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf \
-cp app.jar ComplexJDKClassLoaderExample
使用上一步创建的app.aotconf配置来创建缓存到app.aot文件中
java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf \
-XX:AOTCache=app.aot -cp app.jar
这一步只是创建缓存,并不运行程序,之后可能会把缓存和运行合并成一个步骤。
使用缓存文件运行应用程序:
java -XX:AOTCache=app.aot -cp app.jar ComplexJDKClassLoaderExample
// 如果缓存文件不可用或不存在,则 JVM 会发出警告信息并继续运行
测试启动时间
直接运行
time java -cp app.jar ComplexJDKClassLoaderExample
// result
// java -cp app.jar ComplexJDKClassLoaderExample 1.11s user 0.09s system 48% cpu 2.453 total
使用AOT缓存
time java -XX:AOTCache=app.aot -cp app.jar ComplexJDKClassLoaderExample
// result
// java -XX:AOTCache=app.aot -cp app.jar ComplexJDKClassLoaderExample 0.85s user 0.12s system 56% cpu 1.718 total
测试结果
有了 AOT 缓存,JVM 通常会在程序运行到第三步时进行的读取、解析、加载和链接工作就会提前转移到创建缓存的第二步。这样,程序在第三步启动的速度会更快,因为程序的类可以立即从缓存中获得。 上面的测试程序中,虽然很短,但它使用了文件操作、流 API、反射、HTTP请求、并发操作、时间处理等,从而导致近760个JDK类被读取、解析、加载和链接。 该程序在 JDK24-ea上直接运行需要2.453秒。 在完成创建 AOT 缓存所需的少量额外工作后,该程序的运行时间为1.718秒,提高了30%。 AOT缓存占用25兆字节。而jdk官网测试结果则是提高了42%左右。
AOT的历史
其实AOT Cache是HotSpot JVM的一项旧功能--类数据共享(CDS)的自然演进。 CDS于2004年首次在JDK5的更新中引入。它最初的目的是减少在同一台机器上运行的多个Java应用程序的内存占用。它通过读取和解析 JDK 类文件来实现这一目标,并将生成的元数据存储在只读归档文件中,随后多个 JVM 进程可以使用相同的虚拟内存页将其直接映射到内存中。 后来CDS也进行了扩展,也能存储应用类的元数据。 如今,CDS 的共享优势已因地址空间布局随机化 (ASLR) 等新的安全实践而减弱,ASLR使文件映射到内存中的地址变得不可预测。 不过,CDS 仍能显著改善启动时间--以至于 JDK 12 及更高版本的构建都包含一个内置 CDS 存档,其中包含一千多个常用JDK类的元数据。 AOT 缓存以 CDS 为基础,不仅能提前读取和解析类文件,还能加载和链接类文件。
总结
AOT类加载和链接暂不支持使用ZGC垃圾收集器的jvm程序,除此之外适用于所有现有的 Java 应用程序、库和框架。除了创建AOT缓存这一额外步骤外,它不需要修改源代码,也不需要修改构建配置和代码的启动方式,也不依赖特定工具,只是增加提前培训生成缓存的步骤。 它完全支持Java平台的高度动态特性,包括运行时反射。之所以如此,是因为类读取、解析、加载和链接的时间和顺序对 Java 代码无关紧要。我们完全可以给jvm提供一个训练集,让jvm提前加载和链接这些类,从而缩短应用的启动时间。即使训练集中的类在生产环境中没有使用,也不会影响应用的正常运行。
本文暂时没有评论,来添加一个吧(●'◡'●)