字节码验证是否发生两次?

2022-09-01 11:51:30

因此,我对JVM内部发生的字节码验证有点困惑。根据Deitel和Deitel的书,Java程序要经历五个阶段(编辑,编译,加载,验证和执行)(第1章)。字节码验证器在“验证”阶段验证字节码。书中没有一处提到字节码验证器是类装入器的一部分。

然而,根据oracle的文档,类加载器执行加载,链接和初始化的任务,并且在链接过程中它必须验证字节码。

现在,Deitel和Deitel谈论的字节码验证,以及这个oracle文档谈论的字节码验证,是相同的过程吗?

还是字节码验证发生两次,一次在链接过程中,另一次由字节码验证器进行?

描述Dietel和Dietel在书中提到的Java程序阶段的图片。(我从nobalG:)下面的答案之一中借用了这张照片)enter image description here


答案 1

您可以使用此图表了解字节码验证,该图表在Oracle文档中进行了详细解释

enter image description here

您会发现字节码验证只发生一次而不是两次

该图显示了从 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字节码验证:算法和形式化


答案 2

不。

来自 JVM 规范 4.10

尽管 Java 编程语言的编译器必须仅生成满足前面部分中所有静态和结构约束的类文件,但 Java 虚拟机无法保证要求它加载的任何文件都是由该编译器生成的或格式正确。

然后继续指定验证过程。

JVM 规范 5.4.1

验证 (§4.10) 确保类或接口的二进制表示形式在结构上是正确的 (§4.9)。验证可能会导致加载其他类和接口 (§5.3),但不需要验证或准备它们。

指定链接引用的部分 §4.10 - 不是作为单独的过程,而是加载类的一部分。

JVM和JLS是很好的文档,当你有这样的问题。


推荐