← 返回

JDK 8 vs 17 vs 26:Lambda 解析性能实测

📅 2026-06-10 | 🏷️ JVM, 性能, JMH, JDK | 📖 阅读约 20 分钟


前言

前四篇实现了五条 Lambda 解析路线。这一篇用 JMH 在三个 JDK 版本上跑基准测试,看看:

  1. 五条路线的性能差距到底有多大?
  2. JDK 升级对性能有多大影响?
  3. 实际使用中应该怎么选?

测试环境

硬件: macOS ARM64 (Apple Silicon)
JDK 8:  BellSoft Liberica JDK 8u422 (aarch64)
JDK 17: Homebrew OpenJDK 17.0.19
JDK 26: Homebrew OpenJDK 26.0.1
JMH:    1.37

JMH 参数:

  • Fork = 2(2 个 JVM 进程,避免 JVM 特异性)
  • Warmup = 5(5 轮预热)
  • Measurement = 8(8 轮测量)
  • Mode = AverageTime(平均耗时,越小越好)

重要前提:缓存

实际使用中,所有框架都会缓存解析结果。

// MyBatis Plus 的实际行为:
//
// 第一次调用 User::getAge:
//   → 反射调用 writeReplace()
//   → 提取 SerializedLambda
//   → 解析方法名 → "age"
//   → 构建 PropertyExpression
//   → 缓存到 Map<Class, Map<String, Expression>>
//   → 耗时 ~600 ns(JDK 8)/ ~25 ns(JDK 26)
//
// 后续调用 User::getAge:
//   → 直接读缓存
//   → 耗时 ~1 ns
//
// 所以性能差异只在首次解析时体现
// 运行时的查询性能几乎无差别

下面的基准测试衡量的是首次解析的开销(冷启动)。


场景 1: 提取属性名

User::getAge 中提取属性名 “age”,产出 Expr.property("age")

这是更基础的操作,每条路线都必须做。

                        JDK 8      JDK 17     JDK 26
property_apt              4.1        1.6        1.6    ns/op
property_proxy           32.8       89.3       30.0   ns/op
property_javacPlugin    148.2      133.2      137.3   ns/op
property_methodRef     624.1      269.0       24.6   ns/op
property_asm           652.5      275.2       23.9   ns/op

分析

APT(~1.6 ns)全版本速度高。 编译期生成的元模型,运行时只是读取一个 static final 字段,没有反射、没有字节码解析、没有正则匹配。

MethodRef/ASM 在 JDK 26 上比 JDK 8 快 25 倍。 这个差距非常惊人。原因是 JDK 26 对 SerializedLambda 的反射调用做了深度优化:

  • MethodHandle 缓存(避免每次反射查找方法)
  • JIT 内联优化(把反射调用编译为直接调用)
  • 死代码消除(移除不必要的中间步骤)

Proxy 各版本稳定(~30-90 ns)。 纯反射调用,JVM 优化空间有限。

JavacPlugin 各版本稳定(~135 ns)。 正则表达式的编译和匹配开销,与 JVM 版本无关。


场景 2: 简单比较 age > 18

构建 Expr.compare("GT", Property("age"), Constant(18))

                        JDK 8      JDK 17     JDK 26
simpleExpr_apt            9.0        5.2        5.1    ns/op
simpleExpr_proxy         34.7       91.1       33.3   ns/op
simpleExpr_javacPlugin  155.5      135.2      138.6   ns/op
simpleExpr_methodRef    401.7      209.2       27.3   ns/op
simpleExpr_asm          578.9      210.6       16.9   ns/op

分析

JDK 26 的 ASM 路线(16.9 ns)反而比 MethodRef(27.3 ns)更快了。

这有点意外。分析了一下原因:MethodRef 需要两次反射调用(writeReplace + 方法名解析),而 ASM 在拿到 SerializedLambda 后,直接用 ASM 解析字节码,ASM 的字节码遍历在 JDK 26 上被 JIT 优化得很好。


场景 3: 组合条件 age > 18 AND name = “Tom”

构建 Expr.and(GT(age,18), EQ(name,"Tom"))

                        JDK 8      JDK 17     JDK 26
compositeExpr_apt        20.7       14.8       14.1   ns/op
compositeExpr_proxy      82.1      197.1       75.0   ns/op
compositeExpr_javacPlugin 860.3    951.0      866.8   ns/op
compositeExpr_methodRef  741.4     441.2       64.3   ns/op
compositeExpr_asm      1020.0     436.4       40.1   ns/op

分析

组合条件下,JavacPlugin 变得更慢(~870 ns),因为它需要对每个属性都做一次正则匹配。

JDK 26 的 ASM(40 ns)已经接近 Proxy(75 ns),而 JDK 8 上 ASM 是更慢的(1020 ns)。


批量吞吐量

模拟 ORM 启动时解析 1000 个 getter 的吞吐量(ops/ms,越大越好)。

                        JDK 8      JDK 17     JDK 26
batch_apt              1951      24059      36811    ops/ms
batch_proxy              31         11         33    ops/ms
batch_methodRef           1.7        3.6       37.5  ops/ms
batch_asm                 1.6        3.8       40.3  ops/ms

分析

JDK 26 的 APT 达到 36811 ops/ms,比 JDK 8 快 19 倍。

MethodRef/ASM 从 JDK 8 的 ~1.6 ops/ms 提升到 JDK 26 的 ~40 ops/ms,25 倍

因此:如果你的 ORM 运行在 JDK 26 上,启动时批量解析 1000 个 getter 的耗时从 ~600 ms 降到 ~25 ms。


为什么 JDK 26 快这么多

1. SerializedLambda 反射优化

JDK 26 对 writeReplace() 反射调用做了深度优化。在 JDK 8 中,每次反射调用都需要:

  • 查找方法(Method lookup)
  • 权限检查(access check)
  • 实际调用(invoke)

JDK 26 用 MethodHandle 缓存了查找结果,后续调用直接走缓存,JIT 编译后甚至可以内联为直接调用。

2. ASM 字节码分析路径优化

ASM 的 ClassReaderMethodVisitor 在 JDK 26 上被 JIT 优化得更好:

  • 循环展开(loop unrolling)
  • 分支预测(branch prediction)
  • 内存访问模式优化

3. APT 的天然优势

APT 不依赖 JVM 的反射优化,因为它的运行时代码没有反射调用。全版本都快,天然适合 GraalVM Native Image(AOT 编译)。


实际使用建议

场景推荐路线原因
ORM 常规查询APT + MethodRef写法自然,性能足够
动态条件 / 规则引擎JDK Proxy实现简单,运行时灵活
需要 Lambda 支持ASM (JDK 17+)JDK 17+ 上性能可接受
JDK 8 环境Proxy > APTMethodRef/ASM 在 JDK 8 上首次解析较慢
GraalVM Native ImageAPT无反射,天然兼容 AOT

记住:首次解析的开销只发生一次,后续都是缓存读取(~1 ns)。 选方案时更应该考虑 API 设计和工程复杂度,而不是过度纠结性能。


运行基准测试

git clone https://github.com/linyuliu/lambda-expression-lab.git
cd lambda-expression-lab/benchmark

# 当前 JDK
mvn clean package -q -DskipTests
java -jar target/benchmarks.jar

# 指定 JDK 版本
./run-benchmark.sh 8     # JDK 8
./run-benchmark.sh 17    # JDK 17

# 快速测试(减少迭代)
java -jar target/benchmarks.jar -f 1 -wi 2 -i 3

# 解析日志对比(看每条路线的完整流程)
java -cp target/benchmarks.jar com.example.benchmark.CompareRunner

系列总结

路线首次解析 (JDK 26)缓存读取Lambda 支持适用场景
APT1.6 ns~1 ns编译期元模型
Proxy30 ns~1 ns动态 DSL
ASM24 ns~1 ns运行时 Lambda
MethodRef25 ns~1 ns方法引用
JavacPlugin137 ns~1 ns编译期 AST

核心结论:

  1. APT 速度高,但需要编译期生成代码
  2. JDK 26 对反射调用的优化是革命性的
  3. 缓存是关键,首次解析的开销只发生一次
  4. 选方案时优先考虑 API 设计,其次才是性能

完整项目代码:GitHub - lambda-expression-lab