JDK 8 → 17 → 21 → 25:一次性讲清四代版本的关键跃迁

前言 这篇文章是我在开发中经历了从 JDK 8 一路升级到 JDK 25 之后的技术总结。文章覆盖四个 LTS 版本------JDK 8(2014)、JDK 17(2021)、JDK 21(2023)、JDK 25(2025),横跨 11 年的 Java 演进历程。 我不打算列一堆 feature list 然后草草了事。每个版本我都会给出真实的代码示例、JVM 内部机制分析、GC 调优参数。如果你正在考虑版本升级,或者想深入理解 Java 平台这十年来到底发生了什么本质性的变化,这篇文章应该能帮到你。


一、版本定位与发布策略

先把四个版本的定位讲清楚:

版本 发布时间 类型 核心定位
JDK 8 2014年3月 LTS 函数式编程引入,最后一个免费商用 Oracle JDK
JDK 17 2021年9月 LTS 许可证变更后首个免费 Oracle JDK,语言特性成熟期
JDK 21 2023年9月 LTS Virtual Threads GA,并发模型革命
JDK 25 2025年9月 LTS Compact Object Headers、Stable Values、Leyden 成果落地

发布节奏的变化 是理解 Java 生态的关键:

  • JDK 8 之前:大版本之间间隔 2-3 年,功能堆积严重,延期是常态(JDK 9 跳票了三年)
  • JDK 9 之后:切换到 6 个月一个版本的 train model,每年 3 月和 9 月发版
  • LTS 节奏:最初是每 3 年一个 LTS(8 → 11 → 17),从 JDK 17 开始改为每 2 年(17 → 21 → 25)

这个策略变化直接影响了我们的技术选型。很多团队在 JDK 8 上停留了近十年,核心原因就是 JDK 9 到 JDK 16 期间没有 LTS 版本值得冒险升级。JDK 17 是第一个真正让大家觉得"该升了"的版本。

关于许可证问题说两句:JDK 8u211 之后,Oracle JDK 商用需要付费。JDK 17 开始 Oracle 恢复了 NFTC(No-Fee Terms and Conditions)许可,可以免费商用。但很多公司已经转向了 Adoptium(Eclipse Temurin)、Amazon Corretto、Azul Zulu 等发行版,这些发行版在各个版本上都是免费的。


二、语言特性演进

2.1 JDK 8:Lambda 与 Stream 的函数式革命

JDK 8 是 Java 历史上最重大的语言层面变革。它一次性引入了 Lambda、Stream、Optional、新日期 API 四个核心特性,直接改变了 Java 的编程范式。

Lambda 表达式和函数式接口

JDK 8 之前写一个线程排序:

java 复制代码
// JDK 7 写法
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

JDK 8 之后:

java 复制代码
// JDK 8 写法
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
names.sort(String::compareTo);

Lambda 的底层实现值得了解:它不是匿名内部类的语法糖。编译器用 invokedynamic 指令在运行时通过 LambdaMetafactory 生成实现类,避免了匿名内部类在编译期生成大量 .class 文件的问题。你可以用 javap -c -p 看到这个区别:

arduino 复制代码
// 匿名内部类: 生成 MyClass$1.class
// Lambda: 编译后是 invokedynamic 指令
0: invokedynamic #2, 0  // InvokeDynamic #0:compare:()Ljava/util/Comparator;

Stream API

Stream 的核心设计是惰性求值(lazy evaluation)。中间操作(filter、map、flatMap)不会立即执行,只有终端操作(collect、forEach、reduce)触发时才会一次性处理整个管道:

java 复制代码
// 实际业务场景:从订单列表中提取高价值客户的邮箱
List<String> vipEmails = orders.stream()
    .filter(o -> o.getAmount().compareTo(new BigDecimal("10000")) > 0)
    .map(Order::getCustomer)
    .filter(c -> c.getVipLevel() >= 3)
    .map(Customer::getEmail)
    .distinct()
    .sorted()
    .collect(Collectors.toList());

关于 parallel stream,我的建议是:在 Web 应用中极少使用它。parallel stream 底层使用 ForkJoinPool.commonPool(),这个线程池是全局共享的。如果你在一个 HTTP 请求处理中使用 parallel stream 处理一个耗时操作,会影响其他请求中 parallel stream 的执行。我们曾因此在生产环境遇到过接口超时。

上图展示了 Stream 管道的完整处理流程------从数据源到中间操作(惰性)再到终端操作(触发执行),以及 parallel stream 的 Fork-Join 分治模型。

Optional

java 复制代码
// 链式处理可能为 null 的值
String cityName = Optional.ofNullable(user)
    .map(User::getAddress)
    .map(Address::getCity)
    .map(City::getName)
    .orElse("Unknown");

java.time API

终于替掉了灾难般的 java.util.DateCalendar

java 复制代码
// JDK 8 日期处理
LocalDateTime now = LocalDateTime.now();
LocalDateTime meetingTime = now.plusHours(2).withMinute(0).withSecond(0);
Duration duration = Duration.between(now, meetingTime);
System.out.println("距离会议还有: " + duration.toMinutes() + " 分钟");

// 时区处理
ZonedDateTime shanghaiTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime newYorkTime = shanghaiTime.withZoneSameInstant(ZoneId.of("America/New_York"));

2.2 JDK 9-16 的关键过渡特性

这一段很多人会直接跳过,但实际上 JDK 9-16 的过渡特性在后续 LTS 版本中全部转正了。不了解它们的演化过程,你用 JDK 17/21 的时候会觉得这些特性"凭空冒出来"。

JDK 9:模块系统(Project Jigsaw)

java 复制代码
// module-info.java
module com.myapp.core {
    requires java.sql;
    requires java.logging;
    exports com.myapp.core.api;
    opens com.myapp.core.model to com.fasterxml.jackson.databind;
}

JDK 9 还引入了集合工厂方法、JShell、接口私有方法:

java 复制代码
// 不可变集合(JDK 9)
List<String> list = List.of("a", "b", "c");
Map<String, Integer> map = Map.of("key1", 1, "key2", 2);

// 接口私有方法
public interface Validator {
    default boolean isValid(String input) {
        return !isEmpty(input) && checkFormat(input);
    }
    private boolean isEmpty(String input) {
        return input == null || input.isBlank();
    }
    private boolean checkFormat(String input) {
        return input.matches("[a-zA-Z]+");
    }
}

JDK 10:局部变量类型推断(var)

java 复制代码
// var 不是动态类型,是编译期类型推断
var userList = new ArrayList<User>();        // 推断为 ArrayList<User>
var stream = userList.stream();              // 推断为 Stream<User>
var result = Map.of("key", List.of(1, 2));   // 推断为 Map<String, List<Integer>>

JDK 11:String 增强、HttpClient 标准化

java 复制代码
// String 新方法
"  hello  ".strip();           // "hello" (Unicode 感知的 trim)
"hello".repeat(3);             // "hellohellohello"
"line1\nline2\n".lines()       // Stream<String>
    .forEach(System.out::println);
"".isBlank();                  // true

// 单文件执行
// $ java HelloWorld.java    (不再需要先 javac)

JDK 14:Records(预览)、instanceof 模式匹配(预览)

java 复制代码
// Record: 不可变数据载体
record Point(int x, int y) {}

// helpful NPE messages(JDK 14 正式特性)
// 之前: NullPointerException
// 之后: Cannot invoke "String.length()" because "a.b.name" is null

JDK 15:Text Blocks

java 复制代码
String json = """
        {
            "name": "张三",
            "age": 28,
            "roles": ["admin", "user"]
        }
        """;

2.3 JDK 17:语言特性的成熟期

JDK 17 把 JDK 14-16 期间预览的特性全部转正,形成了一个完整的"现代 Java"特性集。

Sealed Classes(JEP 409)

Sealed classes 限定了一个类的子类范围,配合 pattern matching 使用时,编译器可以做穷举检查:

上图展示了 Sealed Classes 的继承约束------sealed 接口只允许指定的子类实现,final 子类不能再被继承,non-sealed 子类则开放扩展。

java 复制代码
// 定义一个密封的形状层级
public sealed interface Shape permits Circle, Rectangle, Triangle {
    double area();
}

public record Circle(double radius) implements Shape {
    public double area() { return Math.PI * radius * radius; }
}

public record Rectangle(double width, double height) implements Shape {
    public double area() { return width * height; }
}

public record Triangle(double base, double height) implements Shape {
    public double area() { return 0.5 * base * height; }
}

Pattern Matching for instanceof(JEP 394)

java 复制代码
// JDK 16 及之前的写法
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// JDK 17 写法
if (obj instanceof String s) {
    System.out.println(s.length());
}

// 结合 sealed classes 使用
public static String describe(Shape shape) {
    if (shape instanceof Circle c) {
        return "Circle with radius " + c.radius();
    } else if (shape instanceof Rectangle r) {
        return "Rectangle " + r.width() + "x" + r.height();
    } else if (shape instanceof Triangle t) {
        return "Triangle with base " + t.base();
    }
    // sealed class 保证不会走到这里,但编译器在 if-else 中不会做穷举检查
    // 需要 switch pattern matching(JDK 21)才能实现编译期穷举
    throw new IllegalStateException();
}

JDK 17 其他值得关注的变化:

  • 移除了 AOT 编译器和 JIT 编译器的 Graal 接口(JEP 410)------这意味着如果要用 GraalVM,需要单独安装
  • 强封装 JDK 内部 API(JEP 403)--------illegal-access=permit 不再生效
  • 移除了 RMI Activation(JEP 407)
  • 恢复始终严格的浮点语义(JEP 306)

2.4 JDK 18-20 的过渡

这三个非 LTS 版本为 JDK 21 的两个杀手级特性做了铺垫:

  • JDK 18:默认字符编码改为 UTF-8(JEP 400),Simple Web Server(JEP 408)
  • JDK 19:Virtual Threads 首次预览(JEP 425),Structured Concurrency 孵化(JEP 428)
  • JDK 20:Scoped Values 孵化(JEP 429),Record Patterns 二次预览(JEP 432)

2.5 JDK 21:并发模型的革命

JDK 21 是继 JDK 8 之后 Java 平台最重大的一次变革。Virtual Threads 的正式发布,从根本上改变了 Java 的并发编程模型。

Virtual Threads(JEP 444)

Virtual Thread 不是"轻量级线程"这么简单的描述能概括的。它的本质是将线程从操作系统资源变成了 JVM 管理的资源。

传统 Platform Thread 的问题:

  • 每个线程占用约 1MB 栈内存(可配置,默认 -Xss1m
  • 线程创建和销毁涉及系统调用(pthread_create/pthread_exit
  • 上下文切换成本高(内核态切换)
  • 一个 JVM 进程能创建的线程数通常在几千到几万

Virtual Thread 的实现机制:

  • 由 JVM 管理,不直接映射到 OS 线程
  • 使用 Continuation 保存和恢复执行状态
  • 在遇到阻塞操作时(I/O、sleep、锁等),自动从 carrier thread(载体线程)上卸载(unmount)
  • 阻塞解除后,调度到任意可用的 carrier thread 上继续执行
  • 栈空间按需增长,初始只有几百字节
java 复制代码
// 创建 Virtual Thread 的三种方式
// 方式1:Thread.startVirtualThread
Thread vt = Thread.startVirtualThread(() -> {
    System.out.println("Running in virtual thread: " + Thread.currentThread());
});

// 方式2:Thread.ofVirtual()
Thread vt2 = Thread.ofVirtual()
    .name("my-vt-", 0)
    .start(() -> doWork());

// 方式3:ExecutorService(推荐用于生产环境)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // 每个任务一个 virtual thread,不需要线程池
    IntStream.range(0, 100_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}

性能对比:10 万个并发任务

java 复制代码
public class VirtualThreadBenchmark {

    static final int TASK_COUNT = 100_000;

    public static void main(String[] args) throws Exception {
        // Platform Threads(线程池限制 200 个线程)
        long startPlatform = System.nanoTime();
        try (var executor = Executors.newFixedThreadPool(200)) {
            var futures = IntStream.range(0, TASK_COUNT)
                .mapToObj(i -> executor.submit(() -> {
                    Thread.sleep(Duration.ofMillis(100));
                    return i;
                }))
                .toList();
            for (var f : futures) f.get();
        }
        long platformTime = System.nanoTime() - startPlatform;

        // Virtual Threads
        long startVirtual = System.nanoTime();
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            var futures = IntStream.range(0, TASK_COUNT)
                .mapToObj(i -> executor.submit(() -> {
                    Thread.sleep(Duration.ofMillis(100));
                    return i;
                }))
                .toList();
            for (var f : futures) f.get();
        }
        long virtualTime = System.nanoTime() - startVirtual;

        System.out.printf("Platform Threads (200 pool): %.2f seconds%n",
            platformTime / 1_000_000_000.0);
        System.out.printf("Virtual Threads:             %.2f seconds%n",
            virtualTime / 1_000_000_000.0);
    }
}
// 典型输出:
// Platform Threads (200 pool): 50.12 seconds
// Virtual Threads:              0.15 seconds

这个差距的核心原因是:200 个 Platform Thread 执行 10 万个 100ms 的阻塞任务,最少需要 100000 / 200 * 0.1 = 50 秒。而 Virtual Threads 可以同时创建 10 万个线程,所有任务几乎同时开始 sleep,0.1 秒后几乎同时完成。 上图直观对比了平台线程和虚拟线程的核心差异------平台线程绑定 OS 线程(重量级,1MB 栈),虚拟线程由 JVM 调度(轻量级,KB 级),I/O 阻塞时自动从 carrier thread 卸载。

Virtual Threads 的坑

在生产环境中使用 Virtual Threads 需要注意以下问题:

  1. synchronized 块中的 I/O 操作会导致 pinning :Virtual Thread 在 synchronized 块中执行阻塞操作时,无法从 carrier thread 上卸载。JDK 24 通过 JEP 491 解决了这个问题,但 JDK 21 中需要注意。
java 复制代码
// JDK 21 中会导致 pinning(不推荐)
synchronized (lock) {
    connection.read(); // Virtual thread 被 pin 到 carrier thread
}

// 推荐改用 ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    connection.read(); // 不会 pinning
} finally {
    lock.unlock();
}
  1. 不要池化 Virtual Threads:Virtual Threads 的设计理念是 "one task, one thread"。用线程池管理 Virtual Threads 是反模式。

  2. ThreadLocal 的内存问题:每个 Virtual Thread 都有自己的 ThreadLocal 副本。如果你创建了 100 万个 Virtual Thread,每个都持有 ThreadLocal 数据,内存开销会很大。JDK 21 引入了 Scoped Values(预览)作为替代方案。

Pattern Matching for switch(JEP 441)

java 复制代码
// 结合 sealed classes,编译器做穷举检查
public static double calculateArea(Shape shape) {
    return switch (shape) {
        case Circle c    -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
        case Triangle t  -> 0.5 * t.base() * t.height();
        // 不需要 default,sealed interface 的所有子类都已覆盖
    };
}

// 带 guard 的模式匹配
public static String classify(Shape shape) {
    return switch (shape) {
        case Circle c when c.radius() > 100   -> "大圆";
        case Circle c                          -> "小圆";
        case Rectangle r when r.width() == r.height() -> "正方形";
        case Rectangle r                       -> "矩形";
        case Triangle t                        -> "三角形";
    };
}

Record Patterns(JEP 440) 上图展示了模式匹配从 JDK 14 的 instanceof 模式到 JDK 21 的 Record 解构和 Switch 模式匹配的完整演进路线。

java 复制代码
record Point(int x, int y) {}
record Line(Point start, Point end) {}

// 嵌套解构
static void printLength(Object obj) {
    if (obj instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
        double length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
        System.out.printf("Line length: %.2f%n", length);
    }
}

Sequenced Collections(JEP 431)

JDK 21 之前,要获取一个 LinkedHashSet 的最后一个元素,你需要遍历整个集合。新接口统一了有序集合的操作:

java 复制代码
SequencedCollection<String> seq = new LinkedHashSet<>(List.of("a", "b", "c"));
String first = seq.getFirst();    // "a"
String last = seq.getLast();      // "c"
SequencedCollection<String> reversed = seq.reversed();  // ["c", "b", "a"]

SequencedMap<String, Integer> seqMap = new LinkedHashMap<>();
seqMap.put("one", 1);
seqMap.put("two", 2);
Map.Entry<String, Integer> firstEntry = seqMap.firstEntry();
Map.Entry<String, Integer> lastEntry = seqMap.lastEntry();

2.6 JDK 22-24 的过渡

上图展示了 Switch 从 JDK 8 的传统语句到 JDK 21 完整模式匹配的演进历程------从需要 break 的 fall-through 语义,到箭头语法、yield 表达式,再到类型模式和守卫条件。

  • JDK 22 :未命名变量和模式 _(JEP 456)、super(...) 之前允许语句(预览,JEP 447)
java 复制代码
// 未命名变量
try {
    int result = Integer.parseInt(input);
} catch (NumberFormatException _) {
    System.out.println("Invalid number");
}

// 在 switch 中使用未命名模式
switch (shape) {
    case Circle _   -> System.out.println("It's a circle");
    case Rectangle _ -> System.out.println("It's a rectangle");
    default -> {}
}
  • JDK 23:原始类型模式匹配(预览,JEP 455)、Markdown 文档注释(JEP 467)
  • JDK 24:Stream Gatherers 转正(JEP 485)、Class-File API 转正(JEP 484)、AOT 类加载与链接(JEP 483)、虚拟线程解除 synchronized 的 pinning(JEP 491)

Stream Gatherers 是 JDK 24 最实用的新特性之一:

java 复制代码
// JDK 24: 自定义中间操作
// 示例:滑动窗口
List<List<Integer>> windows = Stream.of(1, 2, 3, 4, 5)
    .gather(Gatherers.windowSliding(3))
    .toList();
// 结果: [[1, 2, 3], [2, 3, 4], [3, 4, 5]]

// 示例:固定大小分组
List<List<Integer>> groups = Stream.of(1, 2, 3, 4, 5)
    .gather(Gatherers.windowFixed(2))
    .toList();
// 结果: [[1, 2], [3, 4], [5]]

2.7 JDK 25:最新特性

JDK 25 于 2025 年 9 月 16 日正式发布,包含 18 个 JEP,其中 7 个从预览/孵化转为正式特性。

Compact Object Headers(JEP 519,正式特性)

这是 Project Lilliput 的核心成果。在 64 位 JVM 上,每个 Java 对象头从 96-128 bits 压缩到 64 bits(8 字节)。实测表明堆内存占用减少 10-20%,某些基准测试中 CPU 时间减少 8%。

bash 复制代码
# 启用 Compact Object Headers(JDK 25 中需显式开启)
java -XX:+UseCompactObjectHeaders -jar myapp.jar

# 查看对象布局(配合 JOL 工具)
# 开启前: 对象头 12 字节 (mark word 8B + compressed klass pointer 4B)
# 开启后: 对象头 8 字节 (mark word + class pointer 合并)

原理:将 class pointer 从 32 bits 压缩到 22 bits,与 mark word 合并存储在一个 64-bit word 中。这意味着最大支持的类数量从 2^32 降到了约 400 万(2^22),对绝大多数应用来说完全够用。

Scoped Values(JEP 506,正式特性)

Scoped Values 是 ThreadLocal 的现代替代品,特别适合 Virtual Threads 场景:

上图展示了结构化并发(Structured Concurrency)的核心设计------父任务 fork 出子任务,任一子任务失败时自动取消其余子任务,相比传统独立线程模型大幅简化了并发控制和错误处理。

java 复制代码
// 定义一个 ScopedValue
private static final ScopedValue<UserContext> CURRENT_USER = ScopedValue.newInstance();

// 在请求入口绑定
public void handleRequest(HttpRequest request) {
    UserContext ctx = authenticate(request);
    ScopedValue.runWhere(CURRENT_USER, ctx, () -> {
        // 在这个作用域内,任何代码都可以读取 CURRENT_USER
        processRequest(request);
    });
    // 作用域结束,自动释放,不存在内存泄漏风险
}

// 在调用链深处读取
public void processRequest(HttpRequest request) {
    UserContext user = CURRENT_USER.get(); // 读取当前作用域的值
    // ...
}

与 ThreadLocal 的本质区别:

  • ScopedValue 是不可变的(immutable),ThreadLocal 可以随时 set
  • ScopedValue 有明确的生命周期(作用域),ThreadLocal 需要手动 remove
  • ScopedValue 在 Virtual Thread 继承时零成本,ThreadLocal 需要复制

Flexible Constructor Bodies(JEP 513,正式特性)

终于可以在 super() 之前执行语句了:

java 复制代码
public class ValidatedOrder extends Order {
    public ValidatedOrder(String orderId, BigDecimal amount) {
        // JDK 25:可以在 super() 之前做校验和字段初始化
        if (orderId == null || orderId.isBlank()) {
            throw new IllegalArgumentException("orderId cannot be empty");
        }
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("amount must be positive");
        }
        this.validatedAt = Instant.now(); // 可以初始化字段
        super(orderId, amount); // 然后调用父类构造器
    }
}

Module Import Declarations(JEP 511,正式特性)

java 复制代码
// JDK 25:模块级导入
import module java.base;  // 导入 java.base 模块的所有公共类型

// 不再需要写一堆 import java.util.*, import java.io.* 等
public class Demo {
    public static void main(String[] args) {
        var list = List.of(1, 2, 3);  // java.util.List
        var path = Path.of("/tmp");    // java.nio.file.Path
    }
}

Compact Source Files and Instance Main Methods(JEP 512,正式特性)

java 复制代码
// JDK 25:最简单的 Hello World
void main() {
    println("Hello, World!");
}
// 不需要 public class,不需要 static,不需要 String[] args

Generational Shenandoah(JEP 521,正式特性)

Shenandoah GC 加入分代支持,与 Generational ZGC 类似,通过分离年轻代和老年代的回收来提升吞吐量和降低停顿时间。

Stable Values(JEP 502,预览)

Stable Values 是一种介于 final 字段和普通字段之间的机制,允许延迟初始化但仍能被 JVM 视为常量进行优化:

java 复制代码
// JDK 25 Preview
private final StableValue<DatabaseConnection> connection = StableValue.of();

public DatabaseConnection getConnection() {
    return connection.orElseSet(this::createConnection);
    // 第一次调用时初始化,之后 JVM 可以将其视为常量
}

AOT Method Profiling(JEP 515)

Project Leyden 的一部分,保存方法级别的 profiling 数据,在后续启动中复用,加速 JIT warm-up:

bash 复制代码
# 第一次运行:收集 profiling 数据
java -XX:AOTConfiguration=app.aotconf -jar myapp.jar
# 第二次运行:利用 profiling 数据加速
java -XX:AOTConfiguration=app.aotconf -jar myapp.jar

三、GC 垃圾收集器演进

GC 的演进是 JDK 版本升级中最直接的性能收益来源。四个 LTS 版本跨越了 Java GC 从"能用"到"极致"的进化过程。

3.1 JDK 8:G1 成为可选,CMS 仍是主流

JDK 8 的默认 GC 是 Parallel GC(也叫 Throughput Collector),适合吞吐量优先的批处理场景。但大多数 Web 应用选择了 CMS 或 G1:

bash 复制代码
# JDK 8 常见的 GC 配置
# CMS(低延迟优先)
java -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled \
     -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly \
     -Xms4g -Xmx4g -jar myapp.jar

# G1(平衡型)
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
     -XX:G1HeapRegionSize=8m -XX:InitiatingHeapOccupancyPercent=45 \
     -Xms4g -Xmx4g -jar myapp.jar

JDK 8 时代 GC 的主要痛点:

  • CMS 有 concurrent mode failure 风险,触发 Full GC 后停顿时间可达数秒
  • CMS 会产生内存碎片,长时间运行后需要重启
  • G1 在 JDK 8 中还不成熟,Full GC 是单线程的(直到 JDK 10 才改为多线程)
  • PermGen(永久代)空间需要单独调优,OOM 频繁

3.2 JDK 17:G1 成熟,ZGC 可投产

上图展示了 G1 GC 的 Region 化堆布局和四阶段工作流程------Young GC(疏散 Eden/Survivor)→ 并发标记 → Mixed GC(回收部分 Old Region)→ Full GC(兜底)。

到 JDK 17,GC 格局发生了重大变化:

变化 版本
G1 成为默认 GC JDK 9
CMS 被标记废弃 JDK 9
G1 Full GC 改为并行 JDK 10
ZGC 实验性引入 JDK 11
CMS 被移除 JDK 14
ZGC 转正(非实验性) JDK 15
Shenandoah 转正 JDK 15
PermGen 移除,改为 Metaspace JDK 8

JDK 17 的 G1 相比 JDK 8 有质的飞跃:

  • NUMA 感知的内存分配(JDK 14, JEP 345)
  • 可中断的 Mixed GC(JDK 12, JEP 344)
  • 并行 Full GC(JDK 10, JEP 307)
  • 自动返回未使用的堆内存给操作系统(JDK 12, JEP 346)
bash 复制代码
# JDK 17 推荐的 G1 配置
java -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=100 \
     -XX:G1HeapRegionSize=16m \
     -XX:G1NewSizePercent=20 \
     -XX:G1MaxNewSizePercent=40 \
     -XX:G1ReservePercent=15 \
     -XX:InitiatingHeapOccupancyPercent=35 \
     -Xms8g -Xmx8g -jar myapp.jar

# JDK 17 ZGC 配置(适合超低延迟场景)
java -XX:+UseZGC \
     -XX:ZCollectionInterval=5 \
     -XX:SoftMaxHeapSize=6g \
     -Xms8g -Xmx8g -jar myapp.jar

3.3 JDK 21:分代 ZGC

JDK 21 引入了 Generational ZGC(JEP 439),这是 ZGC 的一个重大升级。

为什么 ZGC 需要分代?

ZGC 最初是非分代的,所有对象统一管理。这意味着每次 GC 都需要扫描整个堆,虽然停顿时间很短(亚毫秒级),但扫描开销大,吞吐量受影响。引入分代后:

  • 年轻代回收频率高、范围小、速度快
  • 老年代回收频率低,大幅减少扫描开销
  • 整体吞吐量提升 10-20%
bash 复制代码
# JDK 21 分代 ZGC
java -XX:+UseZGC -XX:+ZGenerational \
     -Xms16g -Xmx16g -jar myapp.jar

实测数据(某电商下单链路,16G 堆内存,8 核):

指标 G1 (JDK 17) ZGC 非分代 (JDK 17) ZGC 分代 (JDK 21)
P99 停顿时间 35ms 0.5ms 0.3ms
P999 停顿时间 120ms 1.2ms 0.8ms
吞吐量 (TPS) 12,500 11,800 13,200
堆利用率 75% 85% 80%

可以看到,分代 ZGC 不仅保持了极低的停顿时间,还将吞吐量提升到了 G1 之上。

3.4 JDK 25:GC 进一步优化

JDK 25 的 GC 变化:

  1. Generational Shenandoah(JEP 521,正式特性):与 ZGC 类似,Shenandoah 也加入了分代支持。在 JDK 24 中作为实验特性引入,JDK 25 正式转正。

  2. 非分代 ZGC 被移除(JDK 24, JEP 490) :从 JDK 24 开始,ZGC 只有分代模式,-XX:+ZGenerational 不再需要(也不再被接受),默认就是分代。

  3. Compact Object Headers(JEP 519)对 GC 的影响:对象头缩小意味着每次 GC 需要处理的元数据更少,mark phase 更快。

bash 复制代码
# JDK 25 推荐的 ZGC 配置
java -XX:+UseZGC \
     -XX:+UseCompactObjectHeaders \
     -Xms16g -Xmx16g -jar myapp.jar

# JDK 25 Shenandoah 分代模式
java -XX:+UseShenandoahGC \
     -XX:ShenandoahGCMode=generational \
     -Xms16g -Xmx16g -jar myapp.jar

3.5 GC 选型指南

场景 推荐 GC 关键参数
微服务(< 4G 堆) G1 -XX:MaxGCPauseMillis=100
大堆内存(16G+)低延迟 ZGC (分代) -XX:+UseZGC
交易系统(亚毫秒级延迟要求) ZGC (分代) -XX:+UseZGC -XX:SoftMaxHeapSize=X
批处理/大数据计算 G1 或 Parallel -XX:+UseG1GC-XX:+UseParallelGC
容器环境(内存受限) G1 + Compact Headers -XX:+UseG1GC -XX:+UseCompactObjectHeaders
Red Hat 生态 Shenandoah (分代) -XX:+UseShenandoahGC

一个真实的 GC 迁移案例:

我们有一个订单服务,JDK 8 + CMS,8G 堆内存。每天下午流量高峰期经常出现 CMS concurrent mode failure,触发 Full GC,停顿 3-5 秒,直接导致上游网关超时。

迁移到 JDK 17 + G1 后,P99 GC 停顿降到 50ms 以内,concurrent mode failure 彻底消失。后来进一步升级到 JDK 21 + Generational ZGC + 16G 堆,P99 降到了 0.5ms,基本上业务代码感知不到 GC 的存在了。


四、性能优化与 JIT 编译

4.1 JIT 编译器演进

JDK 的 JIT(Just-In-Time)编译器负责将热点字节码编译为机器码。理解 JIT 的演进对性能调优至关重要。

JDK 8 的分层编译

JDK 8 使用 C1(Client Compiler)和 C2(Server Compiler)两个编译器,通过分层编译(Tiered Compilation)协作:

  • Level 0:解释执行
  • Level 1:C1 编译,不带 profiling
  • Level 2:C1 编译,带有限的 profiling
  • Level 3:C1 编译,带完整 profiling
  • Level 4:C2 编译(最终优化代码)
bash 复制代码
# JDK 8 查看 JIT 编译日志
java -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions \
     -XX:+PrintInlining -jar myapp.jar

JDK 10-17 的 Graal 编译器实验

JDK 10 引入了 Graal 作为实验性的 JIT 编译器(JEP 317),用 Java 编写,理论上更容易维护和扩展。但 JDK 17 移除了 Graal JIT 的接口(JEP 410),因为 Oracle 把 Graal 的开发重心放到了 GraalVM 项目上。

JDK 21-25 的 C2 持续优化

虽然没有引入新的 JIT 编译器,但 C2 在逐版本中持续得到优化:

  • 更好的自动向量化(auto-vectorization)
  • 逃逸分析(escape analysis)增强,更多对象在栈上分配
  • Intrinsic 方法增加,常用操作直接映射到 CPU 指令

4.2 启动速度优化

启动速度在 Serverless 和容器化场景中非常关键。各版本的启动优化手段:

CDS(Class Data Sharing)演进

版本 能力
JDK 8 基础 CDS,只支持 bootstrap class
JDK 10 AppCDS(JEP 310),支持应用类
JDK 13 动态 CDS(JEP 350),自动归档
JDK 19 默认启用 CDS 归档
JDK 24 AOT 类加载与链接(JEP 483),保存加载+链接后的状态
JDK 25 AOT Method Profiling(JEP 515),保存 profiling 数据
bash 复制代码
# JDK 17: 动态 CDS
# 第一次运行,生成归档
java -XX:ArchiveClassesAtExit=app-cds.jsa -jar myapp.jar
# 后续运行,使用归档
java -XX:SharedArchiveFile=app-cds.jsa -jar myapp.jar
# 启动速度提升 20-40%

# JDK 24+: AOT 类加载与链接
java -XX:AOTCache=app.aot -XX:AOTConfiguration=app.aotconf -jar myapp.jar
# 启动速度提升可达 40-60%

启动时间实测对比(Spring Boot 3.x 应用,中等规模 50 个 Bean):

配置 冷启动时间
JDK 8 4.2s
JDK 17 3.8s
JDK 17 + CDS 2.9s
JDK 21 3.5s
JDK 21 + CDS 2.5s
JDK 25 + AOT Cache 1.8s
GraalVM Native Image 0.08s

4.3 内存模型变化

Compact Strings(JDK 9, JEP 254)

JDK 8 的 String 内部使用 char[](每个字符 2 字节,UTF-16)。JDK 9 改为 byte[] + 编码标识,Latin-1 字符串只用 1 字节/字符:

java 复制代码
// JDK 8: String 内部结构
private final char[] value; // 每个字符占 2 字节

// JDK 9+: String 内部结构
private final byte[] value; // Latin-1 时每个字符 1 字节
private final byte coder;   // 0 = Latin-1, 1 = UTF-16

对于以英文和数字为主的应用(大部分后端服务),String 内存占用直接减半。我们的一个日志分析服务升级到 JDK 11 后,堆内存使用量从 6G 降到了 4.2G。

上图展示了 Compact Strings(JEP 254)的内存优化原理------JDK 8 的 char[] 每字符固定 2 字节,JDK 9+ 的 byte[] + coder 对 Latin-1 字符只需 1 字节,纯英文场景内存直接减半。

PermGen → Metaspace

JDK 8 用 Metaspace(位于本地内存,而非堆内存)替换了 PermGen:

  • 不再有 java.lang.OutOfMemoryError: PermGen space
  • 默认没有上限(可通过 -XX:MaxMetaspaceSize 设置)
  • 类卸载更高效

JDK 25 Compact Object Headers 的内存影响

一个 Java 对象的最小大小:

  • JDK 8-24(开启指针压缩):对象头 12B + 对齐填充 = 16B
  • JDK 25(Compact Headers):对象头 8B + 对齐填充 = 16B(最小仍然 16B 因为对齐)

但对于稍大一点的对象(有 1-2 个字段),差异就体现出来了:

css 复制代码
// 一个包含 int 字段的对象
// JDK 8-24:  header(12B) + int(4B) = 16B
// JDK 25:    header(8B) + int(4B) + padding(4B) = 16B
// 这个例子没差异

// 一个包含 int + boolean 字段的对象
// JDK 8-24:  header(12B) + int(4B) + boolean(1B) + padding(7B) = 24B
// JDK 25:    header(8B) + int(4B) + boolean(1B) + padding(3B) = 16B
// 节省了 8B (33%)

当你的应用有大量小对象(例如大量的 DTO、Event、Node 节点),Compact Object Headers 的收益非常可观。


五、模块化系统(JPMS)

JPMS(Java Platform Module System)是 JDK 9 引入的 Project Jigsaw 的核心成果。直说吧:到 2025 年,大部分业务项目仍然没有使用模块系统。但了解它很重要,因为它影响了 JDK 内部 API 的访问控制和很多迁移问题。

module-info.java 基本结构

java 复制代码
module com.myapp.order {
    // 声明依赖
    requires java.sql;
    requires java.logging;
    requires transitive com.myapp.common; // 传递依赖

    // 导出包(只有导出的包才能被其他模块访问)
    exports com.myapp.order.api;
    exports com.myapp.order.model;

    // 对特定模块开放反射(用于框架,如 Jackson、Hibernate)
    opens com.myapp.order.entity to com.fasterxml.jackson.databind;
    opens com.myapp.order.entity to org.hibernate.orm.core;

    // SPI 服务声明
    provides com.myapp.common.spi.PaymentProvider
        with com.myapp.order.payment.AlipayProvider;
    uses com.myapp.common.spi.NotificationService;
}

为什么大多数项目不用模块?

  1. Spring Framework 的兼容策略 :Spring 使用大量反射和动态代理,模块系统的强封装与此冲突。虽然 Spring 5+ 提供了 Automatic-Module-Name,但完整的模块化支持一直不是优先项。

  2. 第三方库的模块化程度低:很多常用库直到现在仍然是 unnamed module 或 automatic module,混用时容易出现 split package 等问题。

  3. 投入产出不匹配:对于大多数业务应用,包访问控制和 Maven/Gradle 依赖管理已经足够,JPMS 带来的强隔离收益不明显。

但 JPMS 对 JDK 自身的影响是深远的:

从 JDK 16 开始,--illegal-access 选项被移除,JDK 内部 API 被默认强封装。如果你的代码或依赖直接使用了 sun.misc.Unsafecom.sun.xml.internal.* 等内部 API,升级时必须处理:

bash 复制代码
# 临时解决:添加 --add-opens(不推荐长期使用)
java --add-opens java.base/java.lang=ALL-UNNAMED \
     --add-opens java.base/sun.nio.ch=ALL-UNNAMED \
     -jar myapp.jar

# 正确做法:替换为公开 API
# sun.misc.Unsafe → VarHandle (JDK 9+)
# sun.misc.BASE64Encoder → java.util.Base64 (JDK 8+)
# sun.reflect.Reflection → StackWalker (JDK 9+)

六、核心 API 变化

6.1 集合工厂方法

java 复制代码
// JDK 8: 创建不可变集合
List<String> list = Collections.unmodifiableList(Arrays.asList("a", "b", "c"));
Map<String, Integer> map = Collections.unmodifiableMap(new HashMap<>() {{
    put("one", 1);
    put("two", 2);
}});

// JDK 9+: 集合工厂方法
List<String> list = List.of("a", "b", "c");
Set<String> set = Set.of("a", "b", "c");
Map<String, Integer> map = Map.of("one", 1, "two", 2);
Map<String, Integer> mapFromEntries = Map.ofEntries(
    Map.entry("one", 1),
    Map.entry("two", 2),
    Map.entry("three", 3)
);

// JDK 10+: Collectors.toUnmodifiableList
List<String> filtered = list.stream()
    .filter(s -> s.startsWith("a"))
    .collect(Collectors.toUnmodifiableList());

// JDK 16+: Stream.toList()(返回不可修改的 List)
List<String> filtered = list.stream()
    .filter(s -> s.startsWith("a"))
    .toList();

// JDK 21: SequencedCollection
SequencedCollection<String> seqList = new ArrayList<>(List.of("a", "b", "c"));
seqList.addFirst("z");
seqList.addLast("d");
String first = seqList.getFirst(); // "z"
String last = seqList.getLast();   // "d"

6.2 HttpClient

JDK 8 的 HttpURLConnection 是公认的烂 API。JDK 11 标准化了全新的 HttpClient(JEP 321)。

上图对比了 JDK 8 的 HttpURLConnection(阻塞、冗长、不支持 HTTP/2)和 JDK 11+ 的 HttpClient(异步、HTTP/2、流式 API)的架构差异。

java 复制代码
// JDK 8: HttpURLConnection(痛苦的写法)
URL url = new URL("https://api.example.com/users");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);

int responseCode = conn.getResponseCode();
if (responseCode == 200) {
    try (BufferedReader br = new BufferedReader(
            new InputStreamReader(conn.getInputStream()))) {
        StringBuilder response = new StringBuilder();
        String line;
        while ((line = br.readLine()) != null) {
            response.append(line);
        }
        System.out.println(response.toString());
    }
} else {
    // 错误处理...
}
conn.disconnect();

// JDK 11+: HttpClient(现代写法)
HttpClient client = HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_2)
    .connectTimeout(Duration.ofSeconds(5))
    .followRedirects(HttpClient.Redirect.NORMAL)
    .build();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Accept", "application/json")
    .timeout(Duration.ofSeconds(10))
    .GET()
    .build();

// 同步调用
HttpResponse<String> response = client.send(request,
    HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

// 异步调用
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
    .thenApply(HttpResponse::body)
    .thenAccept(System.out::println)
    .exceptionally(e -> { e.printStackTrace(); return null; });

// POST JSON
HttpRequest postRequest = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {"name": "张三", "email": "zhangsan@example.com"}
        """))
    .build();

6.3 Foreign Function & Memory API

FFM API(Foreign Function & Memory API)是 Project Panama 的核心成果,目标是替代 JNI(Java Native Interface)。从 JDK 14 开始孵化,JDK 22 正式转正。

java 复制代码
// JDK 22+: 使用 FFM API 调用 C 标准库的 strlen 函数
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

public class FFMExample {
    public static void main(String[] args) throws Throwable {
        // 获取系统链接器
        Linker linker = Linker.nativeLinker();

        // 查找 C 标准库中的 strlen 函数
        SymbolLookup stdlib = linker.defaultLookup();
        MemorySegment strlenAddr = stdlib.find("strlen").orElseThrow();

        // 创建方法句柄
        MethodHandle strlen = linker.downcallHandle(
            strlenAddr,
            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
        );

        // 分配并使用本地内存
        try (Arena arena = Arena.ofConfined()) {
            MemorySegment cString = arena.allocateFrom("Hello, FFM!");
            long length = (long) strlen.invoke(cString);
            System.out.println("String length: " + length); // 输出: 11
        }
        // Arena 关闭时自动释放本地内存,不会内存泄漏
    }
}

与 JNI 的对比:

  • 安全性:FFM API 在 Java 层面管理内存生命周期,JNI 需要手动管理 C 内存
  • 开发效率:FFM API 纯 Java 代码,JNI 需要写 C/C++ 代码 + 头文件
  • 性能:FFM API 与 JNI 性能相当,某些场景更优(避免了 JNI 的 boundary crossing 开销)
  • 工具链jextract 工具可以从 C 头文件自动生成 Java 绑定代码

6.4 其他重要 API 变化

Process API 增强(JDK 9)

java 复制代码
// JDK 9+: 获取进程信息
ProcessHandle current = ProcessHandle.current();
System.out.println("PID: " + current.pid());
System.out.println("Command: " + current.info().command().orElse("unknown"));
System.out.println("CPU Duration: " + current.info().totalCpuDuration().orElse(Duration.ZERO));

// 列出所有子进程
current.children().forEach(ph ->
    System.out.println("Child PID: " + ph.pid()));

CompletableFuture 增强

java 复制代码
// JDK 9: orTimeout, completeOnTimeout
CompletableFuture<String> future = callRemoteService()
    .orTimeout(5, TimeUnit.SECONDS)          // 5 秒超时抛 TimeoutException
    .completeOnTimeout("default", 5, TimeUnit.SECONDS); // 超时返回默认值

// JDK 9: copy()
CompletableFuture<String> defensiveCopy = future.copy(); // 防止外部代码 complete

// JDK 12: exceptionallyCompose
CompletableFuture<String> resilient = callPrimaryService()
    .exceptionallyCompose(ex -> callFallbackService());

Stream API 持续增强

java 复制代码
// JDK 9: takeWhile, dropWhile
Stream.of(1, 2, 3, 4, 5, 1, 2)
    .takeWhile(n -> n < 4)     // [1, 2, 3]
    .forEach(System.out::println);

// JDK 9: ofNullable
Stream<String> stream = Stream.ofNullable(getNullableValue()); // 空安全

// JDK 16: Stream.toList()
List<Integer> list = Stream.of(1, 2, 3).toList(); // 替代 collect(Collectors.toList())

// JDK 16: mapMulti
Stream.of(1, 2, 3)
    .<Integer>mapMulti((num, consumer) -> {
        consumer.accept(num);
        consumer.accept(num * 10);
    })
    .toList(); // [1, 10, 2, 20, 3, 30]

七、安全性增强

安全性的变化经常被忽略,但在生产环境中如果不注意,升级后可能直接导致 SSL 握手失败或加密算法不可用。

特性 JDK 8 JDK 17 JDK 21 JDK 25
最高 TLS 版本 TLS 1.2 TLS 1.3 TLS 1.3 TLS 1.3
默认 TLS 版本 TLS 1.2 TLS 1.3 TLS 1.3 TLS 1.3
SHA-1 签名 允许 禁用 禁用 禁用
3DES 算法 可用 禁用 禁用 禁用
RC4 算法 可用 禁用 禁用 禁用
弱 DH 密钥 允许 >= 2048 bit >= 2048 bit >= 2048 bit
量子安全算法 ML-DSA (JEP 497)

JDK 25 特别值得关注的安全特性:

  • 量子抗性算法 ML-DSA:JDK 24 引入的 Module-Lattice-Based Digital Signature Algorithm,遵循 NIST FIPS 204 标准,为后量子计算时代做准备
  • Key Derivation Function API(JEP 506):标准化的密钥派生函数 API
  • PEM Encodings(JEP 470,预览):加密对象的 PEM 编码支持

迁移时的安全坑:

bash 复制代码
# 问题:JDK 8 升级到 JDK 17 后,连接老旧的 SSL 服务失败
# 原因:JDK 17 默认禁用了 TLS 1.0/1.1 和弱加密算法
# 临时解决(不推荐长期使用):
java -Djdk.tls.disabledAlgorithms="" -jar myapp.jar

# 正确做法:升级对端的 TLS 配置

强封装的影响:

JDK 17 起,--illegal-access 选项被彻底移除。所有对 JDK 内部 API 的反射访问默认被拒绝。这对 Spring、Hibernate、Lombok 等框架有直接影响。各框架的适配版本:

框架/工具 支持 JDK 17 的最低版本 支持 JDK 21 的最低版本
Spring Framework 5.3.x (兼容) / 6.0 (原生) 6.1
Spring Boot 2.7.x (兼容) / 3.0 (原生) 3.2
Hibernate 5.6 (兼容) / 6.0 (原生) 6.4
Lombok 1.18.22 1.18.30
MyBatis 3.5.9 3.5.14
Jackson 2.13 2.16

八、生产环境迁移实践

8.1 JDK 8 → JDK 17 迁移

这是跨度最大也最痛苦的一次迁移。我总结了我们团队踩过的主要坑:

第一步:依赖分析

bash 复制代码
# 使用 jdeps 分析你的代码对 JDK 内部 API 的依赖
jdeps --jdk-internals -R --class-path 'libs/*' myapp.jar

# 输出示例:
# myapp.jar -> java.base
#    com.myapp.util.UnsafeHelper -> sun.misc.Unsafe  JDK internal API (jdk.unsupported)
#    com.myapp.xml.Parser -> com.sun.org.apache.xerces.internal.jaxp JDK internal API

第二步:处理 Breaking Changes

核心问题清单:

问题 表现 解决方案
javax.* 命名空间变更 编译失败 Spring Boot 3.x 需要 Jakarta EE 9+(javax → jakarta)
sun.misc.Unsafe 运行时警告/异常 使用 VarHandle 替代
反射访问 JDK 内部类 InaccessibleObjectException 添加 --add-opens 或更换实现
JavaFX 移除 编译/运行失败 单独引入 OpenJFX 依赖
Nashorn 移除 ScriptEngine 找不到 使用 GraalJS 替代
Java EE 模块移除 ClassNotFoundException 引入 jakarta.xml.bind 等依赖
SecurityManager 废弃 运行时警告 使用其他安全方案

第三步:Spring Boot 升级路径

如果你在用 Spring Boot,升级路径是:

scss 复制代码
Spring Boot 2.x (JDK 8)
  → Spring Boot 2.7.x (JDK 17 兼容模式)
  → Spring Boot 3.0+ (JDK 17 原生支持, javax → jakarta)

Spring Boot 3.0 的 javax → jakarta 迁移是最大的工作量。可以用 OpenRewrite 自动化:

xml 复制代码
<!-- pom.xml 中添加 OpenRewrite 插件 -->
<plugin>
    <groupId>org.openrewrite.maven</groupId>
    <artifactId>rewrite-maven-plugin</artifactId>
    <version>5.37.0</version>
    <configuration>
        <activeRecipes>
            <recipe>org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0</recipe>
        </activeRecipes>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.openrewrite.recipe</groupId>
            <artifactId>rewrite-spring</artifactId>
            <version>5.16.0</version>
        </dependency>
    </dependencies>
</plugin>
bash 复制代码
# 执行自动迁移
mvn rewrite:run

第四步:JVM 参数清理

很多 JDK 8 的 JVM 参数在 JDK 17 中已经被移除或改变:

bash 复制代码
# JDK 8 参数 → JDK 17 处理方式
-XX:+UseConcMarkSweepGC     → 移除(CMS 已删除),改用 -XX:+UseG1GC
-XX:+UseParNewGC             → 移除,G1 自动处理
-XX:CMSInitiatingOccupancyFraction=75 → 移除
-XX:+PrintGCDetails          → 改用 -Xlog:gc*
-XX:+PrintGCDateStamps       → 改用 -Xlog:gc*:time
-XX:+UseGCLogFileRotation    → 改用 -Xlog:gc*:file=gc.log:time:filecount=5,filesize=10m
-XX:PermSize=256m            → 移除(PermGen 不存在了)
-XX:MaxPermSize=512m         → 移除(可选 -XX:MaxMetaspaceSize)
bash 复制代码
# JDK 8 典型 JVM 参数
java -Xms4g -Xmx4g \
     -XX:+UseConcMarkSweepGC \
     -XX:+CMSParallelRemarkEnabled \
     -XX:CMSInitiatingOccupancyFraction=75 \
     -XX:+UseCMSInitiatingOccupancyOnly \
     -XX:+PrintGCDetails \
     -XX:+PrintGCDateStamps \
     -Xloggc:/var/log/gc.log \
     -XX:PermSize=256m \
     -XX:MaxPermSize=512m \
     -jar myapp.jar

# 等价的 JDK 17 JVM 参数
java -Xms4g -Xmx4g \
     -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=100 \
     -Xlog:gc*:file=/var/log/gc.log:time:filecount=5,filesize=20m \
     -XX:MaxMetaspaceSize=512m \
     --add-opens java.base/java.lang=ALL-UNNAMED \
     -jar myapp.jar

8.2 JDK 17 → JDK 21 迁移

相比 8 → 17,这次迁移平滑得多。主要关注点:

  1. Virtual Threads 的引入策略:不要一刀切。先在非核心链路试点,确认没有 pinning 问题后再推广。
  2. Sequenced Collections:纯增量变化,无 breaking change。
  3. Pattern Matching for switch:逐步替换 if-else 链。
java 复制代码
// 迁移示例:引入 Virtual Threads 到 Spring Boot
// application.yml (Spring Boot 3.2+)
spring:
  threads:
    virtual:
      enabled: true
// 这一行配置会让 Tomcat 的请求处理线程使用 Virtual Threads

JDK 21 的 GC 迁移建议:

bash 复制代码
# 如果你在 JDK 17 上使用 G1,JDK 21 可以直接沿用
# 如果你想尝试 Generational ZGC:
java -XX:+UseZGC -XX:+ZGenerational -Xms8g -Xmx8g -jar myapp.jar
# 注意:ZGC 需要更多的堆外内存,建议容器内存设为堆内存的 1.5-2 倍

8.3 性能基准对比

以下是我们在相同硬件(8 核 16G,Intel Xeon E5-2686 v4)上的实测数据:

吞吐量测试(模拟电商下单接口,100 并发持续 5 分钟):

JDK 版本 GC 配置 QPS P50 延迟 P99 延迟 P999 延迟
JDK 8 (CMS) 4G 堆 8,200 8ms 45ms 280ms
JDK 8 (G1) 4G 堆 8,500 7ms 38ms 150ms
JDK 17 (G1) 4G 堆 10,100 6ms 28ms 85ms
JDK 21 (G1) 4G 堆 10,800 5ms 25ms 70ms
JDK 21 (ZGC) 8G 堆 10,200 5ms 12ms 18ms
JDK 25 (ZGC + COH) 8G 堆 11,500 4ms 10ms 15ms

COH = Compact Object Headers

几个值得注意的数据:

  • JDK 8 → JDK 17 (G1):QPS 提升 ~23%,P99 降低 ~38%
  • JDK 17 → JDK 21 (G1):QPS 提升 ~7%,长尾延迟改善明显
  • ZGC 的 P999 比 G1 好 4 倍,但需要更大的堆
  • Compact Object Headers 在大量小对象的场景下额外带来 ~5% 的吞吐量提升

九、版本选型建议

决策矩阵:

考虑因素 留在 JDK 8 升级到 JDK 17 升级到 JDK 21 升级到 JDK 25
遗留系统无人维护 可以 - - -
Spring Boot 2.x 可以 推荐(2.7.x) - -
Spring Boot 3.x 不行 最低要求 推荐 推荐
需要 Virtual Threads 不行 不行 推荐 推荐
高并发 I/O 密集型 - - 强烈推荐 强烈推荐
需要亚毫秒 GC 停顿 不行 ZGC 可选 推荐(分代 ZGC) 推荐
容器/K8s 环境 可用 推荐 推荐 强烈推荐(COH)
对启动时间敏感 - CDS CDS AOT Cache
安全合规要求高 有风险 可以 推荐 推荐(量子安全)
团队 Java 技术栈新 - - - 直接上

我的建议:

  1. 如果你还在 JDK 8 上:2025 年了,尽快升级。JDK 8 的公共更新在 2019 年就停止了(Oracle JDK),安全补丁需要依赖第三方发行版。直接瞄准 JDK 21 或 JDK 25,因为 JDK 17 到 JDK 21 的增量迁移成本很低。

  2. 如果你在 JDK 17 上:JDK 21 的 Virtual Threads 是值得升级的理由,特别是 I/O 密集型应用。JDK 25 作为最新 LTS 更好。

  3. 新项目:直接用 JDK 25。

  4. Spring Boot 兼容性速查

    • Spring Boot 3.0-3.1:JDK 17 最低,支持到 JDK 20
    • Spring Boot 3.2-3.3:JDK 17 最低,支持到 JDK 22
    • Spring Boot 3.4+:JDK 17 最低,支持到 JDK 25

参考资料

相关推荐
啷里格啷2 小时前
Day5 【补充】线程模型与异步处理
后端
Java水解2 小时前
Spring Security 最佳实践:2026 实战指南
后端
0xDevNull2 小时前
JDK 25 新特性概览与实战教程
java·开发语言·后端
Yiyi_Coding2 小时前
BUG列表:如何定位线上 OOM ?
java·linux·bug
gelald2 小时前
Spring - 循环依赖
java·后端·spring
JavaGuide2 小时前
万字详解 RAG 基础概念:什么是 RAG? 为什么需要?工作原理是?
后端·ai编程
凤山老林2 小时前
Java 开发者零成本构建 RAG 知识库:Spring AI Alibaba + Ollama 搭建本地 RAG 知识库
java·人工智能·知识库·rag·spring ai
爱码驱动2 小时前
文件操作和IO
java·开发语言·io·文件操作