JDK 8 vs 17 vs 26:Lambda 解析性能实测
📅 2026-06-10 | 🏷️ JVM, 性能, JMH, JDK | 📖 阅读约 20 分钟
前言
前四篇实现了五条 Lambda 解析路线。这一篇用 JMH 在三个 JDK 版本上跑基准测试,看看:
- 五条路线的性能差距到底有多大?
- JDK 升级对性能有多大影响?
- 实际使用中应该怎么选?
测试环境
硬件: 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.37JMH 参数:
- 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 的 ClassReader 和 MethodVisitor 在 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 > APT | MethodRef/ASM 在 JDK 8 上首次解析较慢 |
| GraalVM Native Image | APT | 无反射,天然兼容 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 支持 | 适用场景 |
|---|---|---|---|---|
| APT | 1.6 ns | ~1 ns | ❌ | 编译期元模型 |
| Proxy | 30 ns | ~1 ns | ❌ | 动态 DSL |
| ASM | 24 ns | ~1 ns | ✅ | 运行时 Lambda |
| MethodRef | 25 ns | ~1 ns | ❌ | 方法引用 |
| JavacPlugin | 137 ns | ~1 ns | ✅ | 编译期 AST |
核心结论:
- APT 速度高,但需要编译期生成代码
- JDK 26 对反射调用的优化是革命性的
- 缓存是关键,首次解析的开销只发生一次
- 选方案时优先考虑 API 设计,其次才是性能