您可以使用此图表了解字节码验证,该图表在Oracle文档中进行了详细解释
您会发现字节码验证只发生一次而不是两次
该图显示了从 Java 语言源代码到 Java 编译器,再到类装入器和字节码验证器,再到包含解释器和运行时系统的 Java 虚拟机的数据和控制流。重要的问题是 Java 类装入器和字节码验证器对字节码流的主要来源没有做出任何假设 - 代码可能来自本地系统,或者它可能已经绕地球的一半。字节码验证器充当一种守门人:它确保传递给Java解释器的代码处于适合执行的状态,并且可以运行而不必担心破坏Java解释器。在导入的代码通过验证程序的测试之前,不允许以任何方式执行。验证程序完成后,许多重要属性是已知的:
- 没有操作数堆栈溢出或下溢
- 已知所有字节码指令的参数类型始终是正确的
- 已知对象字段访问是合法的 - 私有、公共或受保护
虽然所有这些检查看起来都非常详细,但当字节码验证器完成其工作时,Java解释器可以继续,因为知道代码将安全运行。了解这些属性使Java解释器更快,因为它不必检查任何内容。没有操作数类型检查,也没有堆栈溢出检查。因此,口译员可以全速运行,而不会影响可靠性。
编辑:-
来自 Oracle Docs Section 5.3.2:
当类装入器 L 的 loadClass 方法以要装入的类或接口 C 的名称 N 调用时,L 必须执行以下两个操作之一才能装入 C:
- 类装入器 L 可以创建一个字节数组,将 C 表示为类文件结构的字节 (§4.1);然后,它必须调用类 ClassLoader 的方法 defineClassClass。调用 defineClass 会导致 Java 虚拟机使用 §5.3.5 中的算法从字节数组中派生出由 N 表示的类或接口,该类或接口使用 L 表示。
- 类装入器 L 可以将 C 的装入委托给其他类装入器 L'。这是通过将参数 N 直接或间接传递给 L' 上的方法调用(通常是 loadClass 方法)来实现的。调用的结果是 C。
正如Holger正确评论的那样,试图通过一个例子来解释它:
static int factorial(int n)
{
int res;
for (res = 1; n > 0; n--) res = res * n;
return res;
}
相应的字节码为
method static int factorial(int), 2 registers, 2 stack slots
0: iconst_1 // push the integer constant 1
1: istore_1 // store it in register 1 (the res variable)
2: iload_0 // push register 0 (the n parameter)
3: ifle 14 // if negative or null, go to PC 14
6: iload_1 // push register 1 (res)
7: iload_0 // push register 0 (n)
8: imul // multiply the two integers at top of stack
9: istore_1 // pop result and store it in register 1
10: iinc 0, -1 // decrement register 0 (n) by 1
11: goto 2 // go to PC 2
14: iload_1 // load register 1 (res)
15: ireturn // return its value to caller
请注意,JVM 中的大多数指令都是类型化的。
现在您应该注意,除非代码至少满足以下条件,否则无法保证 JVM 的正确操作:
- 类型正确性:指令的参数始终是指令预期的类型。
- 没有堆栈溢出或下溢:指令从不从空堆栈中弹出参数,也不会在完整堆栈(其大小等于为方法声明的最大堆栈大小)上推送结果。
- 代码包含:程序计数器必须始终指向方法的代码内,以有效指令编码的开头(不从方法代码的末尾掉落;没有分支进入指令编码的中间)。
- 寄存器初始化:寄存器中的负载必须始终跟随此寄存器中的至少一个存储;换句话说,与方法参数不对应的寄存器不会在方法入口处初始化,并且从未初始化的寄存器加载是错误的。
- 对象初始化:创建类 C 的实例时,必须先调用类 C 的初始化方法之一(对应于此类的构造函数),然后才能使用类实例。
字节码验证的目的是通过在加载时对字节码进行静态分析,一劳永逸地检查这些条件。然后,可以通过验证的字节代码可以更快地执行。
还要注意的是,字节码验证的目的是将上面列出的验证从运行时转移到加载时间。
以上解释摘自Java字节码验证:算法和形式化