云原生 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 5CDS 引入仅 JDK 核心类(rt.jar)
JDK 10AppCDS(应用类共享)JDK 类 + 应用类
JDK 12默认生成 CDS 归档安装时自动生成
JDK 13Archivable 类扩展支持归档更多类
# 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 3QuarkusMicronaut
开发方VMware/BroadcomRed HatMicronaut 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 MB150-300 MB150-300 MB
内存(Native)50-150 MB30-80 MB30-80 MB
镜像大小(Native)80-120 MB50-80 MB50-80 MB
峰值吞吐最高(生态成熟)
Native Image 支持Spring AOT原生设计原生设计
生态丰富度极高中高(Extension)中(需 Micronaut 适配)
学习成本低(Java 开发者熟悉)
热重载Spring DevToolsLive Reload(快)热重载支持
Data 访问Spring Data JPAHibernate/PanacheMicronaut Data
适用场景企业级应用、传统微服务云原生、Serverless云原生、Serverless

选型建议

选型决策树

是否云原生 / Serverless 场景?
├── 否 → Spring Boot(生态成熟、人才多)
└── 是
    │
    是否需要极致启动和内存?
    ├── 是(Serverless/FaaS)
    │   │
    │   团队是否接受 Kotlin/Groovy 生态?
    │   ├── 是 → Micronaut(编译时 DI 更干净)
    │   └── 否 → Quarkus(Java 生态更贴近)
    │
    └── 否(K8s 常驻服务)
        │
        是否愿意引入新框架?
        ├── 是 → Quarkus / Micronaut(JVM 模式也快)
        └── 否 → Spring Boot 3 + CDS(最低改造成本)

实践要点

Quarkus 实践

  1. Extension 优先:只使用有 Quarkus Extension 的库。直接引入传统 Spring 库会退化为运行时反射,失去 Quarkus 优势。
  2. 开发模式./mvnw quarkus:dev 提供秒级热重载和优雅的调试体验,比 Spring DevTools 更快。
  3. Native Image 构建:需要 GraalVM 环境,构建时间 5-15 分钟。建议在 CI 中用专用构建容器。
  4. 配置简化:Quarkus 用 application.properties 统一配置,支持 @ConfigMapping 类型安全配置。

Micronaut 实践

  1. 构造器注入:Micronaut 推荐构造器注入(@Inject 标在构造器上),编译时生成更干净的装配代码。
  2. Micronaut Data:用 Micronaut Data 替代 Spring Data JPA,编译时生成查询实现,Native Image 友好。
  3. AOP 限制:Micronaut 的 AOP 在编译时处理,不支持运行时动态织入。需要动态代理的场景需额外处理。
  4. Bean 作用域:Micronaut 的作用域(@Singleton@Prototype)与 Spring 类似,但语义在编译时确定。

CRIU / CRaC 实践

  1. 环境一致性:快照和恢复环境必须一致(内核版本、CPU 架构)。跨节点恢复需用相同基础镜像。
  2. 资源处理:网络连接、文件描述符、定时任务在 checkpoint 前需处理。使用 CRaC API 注册资源回调。
  3. 堆大小权衡:CRIU 快照包含整个堆,大堆恢复慢。建议快照前触发一次 GC 减小堆体积。
  4. JDK 选择:使用 Azul Zulu CRaC 版或 OpenJDK CRaC 构建,标准 OpenJDK 不支持 CRaC API。

CDS 实践

  1. 最低成本加速:CDS 是唯一无需改代码、无需新框架的启动加速方案,Spring Boot 应用建议首先尝试。
  2. 容器中的 CDS:在 Dockerfile 中生成归档文件,打包到镜像。注意归档文件路径在容器内外一致。
  3. 与 Native Image 互补:CDS 优化 JVM 模式启动,Native Image 是更激进的方案。两者不冲突,按场景选择。
  4. 归档更新:依赖升级后需重新生成归档。在 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 将带来什么变革。

更多推荐