【JVM原理详解】60-云原生JVM-Quarkus与Micronaut与CRIU
云原生 JVM — Quarkus 与 Micronaut 与 CRIU
引言
前两篇我们看了 GraalVM 的 AOT 编译和 JVM 语言生态。在云原生时代,JVM 面临一个尴尬处境:容器按秒计费、Pod 按需伸缩、函数按请求冷启动,而传统 Spring Boot 应用启动要 2-5 秒、内存 300-800 MB、容器镜像 500 MB+——这在 Kubernetes 弹性伸缩和 Serverless 场景下是难以接受的。
这个问题有两条解决路径:一是从框架层改造,把运行时才做的工作提前到构建时,减少启动开销,代表是 Quarkus 和 Micronaut;二是从运行时层改造,用容器快照或类数据共享跳过 JVM 预热,代表是 CRIU 和 CDS。本篇深入这三项技术的原理、对比和实践,并给出 Spring Boot vs Quarkus vs Micronaut 的选型建议。
云原生对 JVM 的挑战
三大痛点
传统 JVM 应用在云原生的痛点
┌──────────────────────────────────────────────────┐
│ 痛点 传统JVM 云原生要求 │
├──────────────────────────────────────────────────┤
│ 启动时间 2-5 秒 < 1 秒(理想<100ms) │
│ 内存占用 300-800 MB < 200 MB │
│ 镜像大小 500 MB+ < 150 MB │
│ 首请求延迟 需预热 首请求即峰值 │
└──────────────────────────────────────────────────┘
根因分析:
1. 类加载:Spring Boot 扫描 classpath,加载数千个类
2. 依赖注入:运行时反射创建 Bean、解析依赖图
3. JIT 预热:解释执行 → C1 → C2 逐层优化,需数千次调用
4. 框架初始化:自动配置、条件装配、AOP 代理生成
Kubernetes 的弹性伸缩加剧了这些问题:流量洪峰到来时 HPA(Horizontal Pod Autoscaler)扩容新 Pod,新 Pod 需要数十秒才能就绪,错过流量高峰;Serverless 平台(AWS Lambda、Knative)的冷启动超时通常只有几秒,传统 JVM 应用经常超时。
两条解决路径
路径一:框架层改造(构建时优化)
Quarkus → build-time 处理 + Native Image
Micronaut → 编译时依赖注入 + AOP
路径二:运行时层改造(启动加速)
CRIU → 容器快照恢复,跳过 JVM 启动
CDS → 类数据共享,减少类加载开销
AppCDS → 应用类共享,进一步加速
Quarkus:Supersonic Subatomic Java
Quarkus 由 Red Hat 开发,号称 “Supersonic Subatomic Java”(超音速亚原子 Java)。它的核心思路是把尽可能多的工作从运行时提前到构建时。
构建时处理
传统框架(如 Spring Boot)在启动时做大量工作:扫描 classpath、解析注解、创建 Bean、生成代理。Quarkus 把这些工作移到构建时:
Quarkus 构建时 vs 运行时
传统框架启动流程(运行时):
1. JVM 启动,加载类
2. 扫描 classpath(反射)
3. 解析注解(反射)
4. 构建 Bean 依赖图
5. 创建 Bean 实例(反射)
6. 生成 AOP 代理(字节码生成)
7. 就绪
耗时:2-5 秒
Quarkus 启动流程(构建时已做完 2-6):
构建时:
1. 扫描 classpath
2. 解析注解
3. 生成 Bean 创建代码(直接 new,无反射)
4. 生成 AOP 代理代码
5. 生成启动引导索引
运行时:
1. JVM 启动
2. 执行预生成的直接代码(无反射)
3. 就绪
耗时:0.05-0.5 秒
Quarkus 的两种模式
Quarkus 支持两种运行模式:
# 模式一:JVM 模式(快速开发,正常部署)
./mvnw package
java -jar target/quarkus-app/quarkus-run.jar
# 启动约 0.7 秒,内存约 150 MB
# 模式二:Native Image 模式(极致启动和内存)
./mvnw package -Pnative
./target/myapp-runner
# 启动约 0.05 秒,内存约 50 MB
代码示例
// Quarkus 应用示例
// 适用 JDK 17+,Quarkus 3.x
@Path("/users")
@ApplicationScoped
public class UserResource {
@Inject
UserRepository repository; // 构建时注入,运行时直接调用
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<User> list() {
return repository.findAll();
}
@GET
@Path("/{id}")
public User get(@PathParam Long id) {
return repository.findById(id);
}
@POST
@Transactional
public Response create(User user) {
repository.persist(user);
return Response.created(URI.create("/users/" + user.id)).build();
}
}
// Panache 实体(简化 ActiveRecord 风格的 ORM)
@Entity
public class User extends PanacheEntity {
public String name;
public int age;
// 静态查询方法,构建时生成实现
public static User findByName(String name) {
return find("name", name).firstResult();
}
}
Quarkus 的关键设计是 Extension(扩展)机制:每个集成(Hibernate、RESTEasy、Kafka 等)以 Quarkus Extension 形式提供,Extension 在构建时处理自己的注解和配置,生成直接的调用代码。这意味着只有支持 Quarkus Extension 的库才能获得构建时优化——直接用传统 Spring 库不会享受到优化。
Micronaut:编译时依赖注入
Micronaut 由 ObjectBay(后独立为 Micronaut Foundation)开发,2018 年首发。它的核心设计是编译时依赖注入和 AOP,彻底消除运行时反射。
编译时 DI 的原理
Micronaut 编译时 DI 流程
源码编译阶段:
1. 注解处理器(APT)扫描 @Inject / @Singleton 等
2. 为每个 Bean 生成 BeanDefinition 类(直接 new,无反射)
3. 为每个 AOP 切面生成代理类
4. 生成依赖注入的装配代码
生成的代码示例(概念):
// 源码
@Singleton
class UserService { @Inject UserRepo repo; }
// 编译时生成
class UserService$BeanDefinition {
UserService instantiate(BeanContext ctx) {
UserService bean = new UserService(); // 直接 new
bean.repo = ctx.getBean(UserRepo.class); // 直接查找
return bean;
}
}
运行时:
启动 → 读取 BeanDefinition → 直接执行装配代码 → 就绪
无反射、无类路径扫描、无代理字节码生成
代码示例
// Micronaut 应用示例
// 适用 JDK 17+,Micronaut 4.x
@Singleton
public class UserService {
private final UserRepository repository;
// 构造器注入(编译时解析,推荐)
@Inject
public UserService(UserRepository repository) {
this.repository = repository;
}
public List<User> findAll() {
return repository.findAll();
}
}
@Controller("/users")
public class UserController {
@Inject
UserService userService;
@Get
public List<User> list() {
return userService.findAll();
}
@Get("/{id}")
public User get(@PathVariable Long id) {
return userService.findById(id);
}
}
// Micronaut Data 编译时生成查询实现
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
// 编译时生成实现,运行时无反射
List<User> findByName(String name);
@Query("SELECT * FROM users WHERE age > :age")
List<User> findByAgeGreaterThan(int age);
}
Micronaut Data 是亮点特性:Repository 接口在编译时由注解处理器生成完整的 SQL 查询和映射实现,运行时无反射、无代理。这与 Spring Data JPA 的运行时代理生成形成对比。
Micronaut 的 AOT 与 Native Image
Micronaut 从设计之初就为 AOT 和 Native Image 优化:
# Micronaut Native Image 构建
./mvnw package -Dpackaging=native-image
./target/myapp
# 启动约 0.03 秒,内存约 40 MB
由于编译时已完成所有依赖注入和 AOP 处理,Micronaut 的 Native Image 构建比传统 Spring 应用简单得多——几乎不需要额外的反射配置。
CRIU:容器快照恢复
CRIU(Checkpoint/Restore In Userspace)是 Linux 的一个工具,能冻结正在运行的进程并保存其状态到磁盘,之后从磁盘恢复进程继续运行。这为 JVM 冷启动问题提供了一个完全不同的思路。
CRIU 工作原理
CRIU 加速 JVM 启动
传统 JVM 冷启动:
JVM 进程启动 → 类加载 → JIT 预热 → 应用就绪
耗时:5-30 秒(取决于应用复杂度)
CRIU 快照恢复:
阶段一:准备快照(离线,只做一次)
1. 启动 JVM 应用
2. 执行预热(发送请求触发 JIT 编译)
3. 等待应用达到稳态(JIT 充分优化)
4. CRIU checkpoint:冻结进程,保存内存/寄存器/文件描述符到镜像
阶段二:快照恢复(在线,每次冷启动)
1. CRIU restore:从镜像恢复进程
2. 进程直接从冻结点继续运行
3. 已编译的 JIT 代码、已加载的类、已初始化的堆全部恢复
耗时:0.1-2 秒(取决于堆大小)
# CRIU 基本操作
# 冻结进程(PID 12345)并保存状态
criu dump -t 12345 --images-dir /checkpoint
# 从快照恢复进程
criu restore --images-dir /checkpoint
CRIU 与容器结合
在 Kubernetes 中,CRIU 可用于容器快照:
Kubernetes + CRIU 流程
1. 构建阶段:
启动容器 → 预热 JVM → criu dump → 保存容器快照到镜像层
2. 扩容阶段:
kubelet 拉起新容器 → criu restore → 应用立即就绪
跳过 JVM 启动 + 类加载 + JIT 预热
实际限制:CRIU 恢复要求运行环境与快照环境一致(内核版本、CPU 架构、文件系统路径),且恢复的 JVM 会丢失网络连接(需要重连)。这些限制让 CRIU 在通用场景推广困难,但在特定平台(如 AWS Firecracker、Azure 的容器快照功能)中已有应用。
与 Project CRaC 的结合
Project CRaC(Coordinated Restore at Checkpoint) 是 OpenJDK 的一个项目,专门为 CRIU 场景优化 JVM。它提供 Java API 让应用感知 checkpoint/restore 事件:
// CRaC API 示例(JDK with CRaC)
public class MyApp implements jdk.crac.Resource {
@Override
public void beforeCheckpoint(Context<? extends Resource> context) {
// 快照前:关闭网络连接、刷新缓冲区
closeConnections();
}
@Override
public void afterRestore(Context<? extends Resource> context) {
// 恢复后:重新建立连接
reconnect();
}
public void start() {
// 注册为 CRaC 资源
jdk.crac.Context.global().register(this);
// 正常业务逻辑
runServer();
}
}
CRaC 让 JVM 配合 CRIU 处理好网络、文件描述符等需要"重连"的资源,使快照恢复更可靠。Azul Zulu JDK 和 OpenJDK CRaC 构建版已支持。
CDS 与 AppCDS 优化
CDS(Class Data Sharing) 是 HotSpot 内置的启动加速技术,原理是把类的元数据(InstanceKlass)预先处理成共享归档文件,启动时直接内存映射,跳过类的解析和验证。
CDS 工作流程
传统类加载:
.class 文件 → 读取 → 解析 → 验证 → 准备 → InstanceKlass
每个类都要走一遍,启动时加载数千个类
CDS 模式:
构建时(一次):
java -Xshare:dump # 生成 classes.jsa 共享归档
运行时(每次启动):
内存映射 classes.jsa → 直接获得 InstanceKlass
跳过解析/验证步骤,多 JVM 进程共享同一份内存
CDS 的演进
| 版本 | 特性 | 共享范围 |
|---|---|---|
| JDK 5 | CDS 引入 | 仅 JDK 核心类(rt.jar) |
| JDK 10 | AppCDS(应用类共享) | JDK 类 + 应用类 |
| JDK 12 | 默认生成 CDS 归档 | 安装时自动生成 |
| JDK 13 | Archivable 类扩展 | 支持归档更多类 |
# AppCDS 使用流程(JDK 11+)
# 步骤一:生成类列表
java -Xshare:off -XX:DumpLoadedClassList=app.classlist \
-jar myapp.jar
# 步骤二:生成共享归档
java -Xshare:dump -XX:SharedClassListFile=app.classlist \
-XX:SharedArchiveFile=app.jsa \
-jar myapp.jar
# 步骤三:使用归档启动
java -Xshare:on -XX:SharedArchiveFile=app.jsa \
-jar myapp.jar
# 启动时间减少 20-40%
CDS 的效果与局限
CDS 主要优化类加载阶段,对 JIT 预热无帮助。对于类加载占比高的应用(如 Spring Boot),CDS 能减少 20-40% 的启动时间;但对计算密集型应用效果有限。
CDS 的优势是无需修改代码、无需 Native Image 构建的复杂配置,是最低成本的启动加速方案。JDK 12+ 默认开启 CDS(使用 JDK 内置归档),开启 AppCDS 只需额外几步配置。
三方对比:Spring Boot vs Quarkus vs Micronaut
| 维度 | Spring Boot 3 | Quarkus | Micronaut |
|---|---|---|---|
| 开发方 | VMware/Broadcom | Red Hat | Micronaut Foundation |
| DI 机制 | 运行时反射 | 构建时处理 | 编译时 APT |
| 启动时间(JVM) | 2-5 秒 | 0.7-1.5 秒 | 0.8-1.5 秒 |
| 启动时间(Native) | 0.05-0.1 秒 | 0.02-0.05 秒 | 0.02-0.05 秒 |
| 内存(JVM) | 300-800 MB | 150-300 MB | 150-300 MB |
| 内存(Native) | 50-150 MB | 30-80 MB | 30-80 MB |
| 镜像大小(Native) | 80-120 MB | 50-80 MB | 50-80 MB |
| 峰值吞吐 | 最高(生态成熟) | 高 | 高 |
| Native Image 支持 | Spring AOT | 原生设计 | 原生设计 |
| 生态丰富度 | 极高 | 中高(Extension) | 中(需 Micronaut 适配) |
| 学习成本 | 低(Java 开发者熟悉) | 中 | 中 |
| 热重载 | Spring DevTools | Live Reload(快) | 热重载支持 |
| Data 访问 | Spring Data JPA | Hibernate/Panache | Micronaut Data |
| 适用场景 | 企业级应用、传统微服务 | 云原生、Serverless | 云原生、Serverless |
选型建议
选型决策树
是否云原生 / Serverless 场景?
├── 否 → Spring Boot(生态成熟、人才多)
└── 是
│
是否需要极致启动和内存?
├── 是(Serverless/FaaS)
│ │
│ 团队是否接受 Kotlin/Groovy 生态?
│ ├── 是 → Micronaut(编译时 DI 更干净)
│ └── 否 → Quarkus(Java 生态更贴近)
│
└── 否(K8s 常驻服务)
│
是否愿意引入新框架?
├── 是 → Quarkus / Micronaut(JVM 模式也快)
└── 否 → Spring Boot 3 + CDS(最低改造成本)
实践要点
Quarkus 实践
- Extension 优先:只使用有 Quarkus Extension 的库。直接引入传统 Spring 库会退化为运行时反射,失去 Quarkus 优势。
- 开发模式:
./mvnw quarkus:dev提供秒级热重载和优雅的调试体验,比 Spring DevTools 更快。 - Native Image 构建:需要 GraalVM 环境,构建时间 5-15 分钟。建议在 CI 中用专用构建容器。
- 配置简化:Quarkus 用
application.properties统一配置,支持@ConfigMapping类型安全配置。
Micronaut 实践
- 构造器注入:Micronaut 推荐构造器注入(
@Inject标在构造器上),编译时生成更干净的装配代码。 - Micronaut Data:用 Micronaut Data 替代 Spring Data JPA,编译时生成查询实现,Native Image 友好。
- AOP 限制:Micronaut 的 AOP 在编译时处理,不支持运行时动态织入。需要动态代理的场景需额外处理。
- Bean 作用域:Micronaut 的作用域(
@Singleton、@Prototype)与 Spring 类似,但语义在编译时确定。
CRIU / CRaC 实践
- 环境一致性:快照和恢复环境必须一致(内核版本、CPU 架构)。跨节点恢复需用相同基础镜像。
- 资源处理:网络连接、文件描述符、定时任务在 checkpoint 前需处理。使用 CRaC API 注册资源回调。
- 堆大小权衡:CRIU 快照包含整个堆,大堆恢复慢。建议快照前触发一次 GC 减小堆体积。
- JDK 选择:使用 Azul Zulu CRaC 版或 OpenJDK CRaC 构建,标准 OpenJDK 不支持 CRaC API。
CDS 实践
- 最低成本加速:CDS 是唯一无需改代码、无需新框架的启动加速方案,Spring Boot 应用建议首先尝试。
- 容器中的 CDS:在 Dockerfile 中生成归档文件,打包到镜像。注意归档文件路径在容器内外一致。
- 与 Native Image 互补:CDS 优化 JVM 模式启动,Native Image 是更激进的方案。两者不冲突,按场景选择。
- 归档更新:依赖升级后需重新生成归档。在 CI 中加入归档生成步骤。
小结
- 云原生挑战:JVM 启动慢、内存大、镜像大,与容器秒级伸缩和 Serverless 冷启动要求冲突。根因是运行时类加载、反射 DI、JIT 预热。
- Quarkus:Red Hat 出品,构建时处理注解和 DI,生成直接调用代码。支持 JVM 模式和 Native Image 模式,Extension 机制保证生态库也享受构建时优化。
- Micronaut:编译时依赖注入(APT 生成 BeanDefinition)和编译时 AOP,运行时零反射。Micronaut Data 编译时生成查询实现,Native Image 友好。
- CRIU + CRaC:容器快照恢复,跳过 JVM 启动和 JIT 预热。CRaC 提供 Java API 处理 checkpoint/restore 的资源管理。限制是环境一致性要求高。
- CDS/AppCDS:HotSpot 内置的类数据共享,内存映射共享归档跳过类解析验证。无需改代码,启动加速 20-40%,最低成本的优化方案。
- 选型核心:传统企业应用选 Spring Boot + CDS,云原生常驻服务选 Quarkus/Micronaut(JVM 模式),Serverless/FaaS 选 Quarkus/Micronaut(Native Image 模式),特定平台可探索 CRIU/CRaC。
下一篇我们展望 JVM 的未来——Project Lilliput 如何缩小对象头、Project Leyden 如何用 AOT 解决冷启动、Valhalla/Babylon/Amber 将带来什么变革。
更多推荐


所有评论(0)