bigywater.tech
Java · 245

字节码验证器:类加载前 JVM 如何检查类型安全

类加载时 JVM 会用字节码验证器检查类型安全。验证什么、什么时候验证、关掉它会怎样?这篇用 ASM 造出七种坏 class,真实触发每一种 VerifyError,再量验证的成本,并看关闭验证后的后果。

7 种
构造并触发的 VerifyError 类型
41 vs 30 ms
定义并链接 3000 个类:验证 / 不验证应用类
崩溃
关闭验证后加载验证不通过的类的结果

七种 VerifyError

实测 · 本机执行输出
  A 栈下溢:iadd 时栈是空的 → defineClass 成功,getMethod 时抛出 VerifyError: Operand stack underflow ← Attempt to pop empty stack.
  B 返回类型不符:iconst_1 再 areturn(方法声明返回 int) → defineClass 成功,getMethod 时抛出 VerifyError: Bad type on operand stack ← Type integer (current frame, stack[0]) is not assignable to reference type
  C 局部变量越界:静态无参方法里 iload_0 → defineClass 成功,getMethod 时抛出 VerifyError: Local variable table overflow ← Local index 0 is invalid
  D 对没有调用 <init> 的对象调用 hashCode(new Object 后直接 invokevirtual) → defineClass 成功,getMethod 时抛出 VerifyError: Bad type on operand stack ← Type uninitialized 0 (current frame, stack[1]) is not assignable to 'java/lang/Object'
  E 分支目标没有栈映射帧(class 版本 52) → defineClass 成功,getMethod 时抛出 VerifyError: Expecting a stackmap frame at branch target 6 ← Expected stackmap frame at this location.
  F 方法末尾没有返回(执行会掉出代码末端) → defineClass 成功,getMethod 时抛出 VerifyError: Control flow falls through code end ← Error exists in the bytecode
  G max_stack 声明得太小(实际要 2,声明 1) → defineClass 成功,getMethod 时抛出 VerifyError: Operand stack overflow ← Exceeded max stack size.

什么时候验证

实测 · 本机执行输出
  defineClass(含有验证不通过的方法的类)→ 成功,返回 Class 对象 t.Bad(此时还没有验证)
  Class.forName(name, initialize=false, loader) → 成功(没有触发链接)
  getDeclaredMethods() → VerifyError: Bad type on operand stack
  getDeclaredConstructor() → VerifyError: Bad type on operand stack
  调用 f() → VerifyError
  第二次调用 → VerifyError(同样失败,每次都是同一个验证错误)
  Class.forName(name, true, loader)→ VerifyError
  对照:合法的类调用 f() = 1
  class 版本 49(Java 5,没有栈映射帧,走旧的类型推断验证器)同一个错误 → VerifyError: (class: t/Old, method: f signature: ()I) Expecting to find object/array on stack

验证成本与关闭验证

实测 · 本机执行输出
  [默认(验证应用类)] 3 次运行(每次内部取最好)的中位:41 ms,每个类 14 μs;范围 40–42 ms
  [-XX:+UnlockDiagnosticVMOptions -XX:-BytecodeVerificationRemote(不验证应用类加载器的类)] 3 次运行(每次内部取最好)的中位:30 ms,每个类 10 μs;范围 29–30 ms
  JVM 输出的警告: []
  不验证(-XX:-BytecodeVerificationRemote)时,iconst_1 再 areturn 的类(验证本来会拒绝):JVM 返回码 -6 ;输出: ['# A fatal error has been detected by the Java Runtime Environment:', '#  Internal Error (0xe0000000), pid=9619, tid=5635', '#  stop: illegal bytecode sequence - method not verified', '# JRE version: OpenJDK Runtime Environment Homebrew (21.0.12.1) (build 21.0.12.1)', '# Java VM: OpenJDK 64-Bit Server VM Homebrew (21.0.12.1, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, bsd-aarch64)', '# Problematic frame:']
  -Xverify:none(已弃用)时同一个类:JVM 返回码 -6 ;输出: ['# A fatal error has been detected by the Java Runtime Environment:', '#  Internal Error (0xe0000000), pid=9632, tid=5379', '#  stop: illegal bytecode sequence - method not verified', '# JRE version: OpenJDK Runtime Environment Homebrew (21.0.12.1) (build 21.0.12.1)', '# Java VM: OpenJDK 64-Bit Server VM Homebrew (21.0.12.1, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, bsd-aarch64)', '# Problematic frame:']

分步演示

字节码验证器
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;class 文件由 ASM 9.7 生成;关闭验证的测试在独立的 JVM 进程里进行,崩溃只影响该进程
    • 没有验证:StackMapTable 的各种帧类型细节、字节码验证器在 JDK 27 的变化、模块与 sealed 相关的验证检查、验证成本占总启动时间的比例(只测了生成类)、-XX:-BytecodeVerificationLocal 对 JDK 自带类的影响;不要在生产里关闭验证
    京ICP备2026067502号-1京公网安备11010502063487号