字节码的验证会发生两次吗?

30

我对JVM内部字节码的验证有些困惑。根据Deitel和Deitel的书籍,Java程序经历五个阶段(编辑、编译、加载、验证和执行)(第1章)。字节码验证器在“验证”阶段验证字节码。该书没有提到字节码验证器是类加载器的一部分。

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

那么,Deitel 和 Deitel提到的字节码验证和此 Oracle文档提到的字节码验证是否相同?

或者字节码验证会发生两次,一次在链接过程中,另一次由字节码验证器执行?

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

5个回答

21
您可以使用此图表来理解字节码验证,详细说明在Oracle文档中。您将发现字节码验证仅发生一次,而不是两次。
该示例显示了从Java语言源代码通过Java编译器、类加载器和字节码验证器到包含解释器和运行时系统的Java虚拟机的数据和控制流程。重要的问题是Java类加载器和字节码验证器对字节码流的主要来源不做任何假设——代码可能来自本地系统,也可能已经跨越半个地球。字节码验证器充当了一个门卫的角色:它确保传递给Java解释器的代码处于适合执行并且可以安全运行的状态,以避免破坏Java解释器。导入的代码在经过验证器的测试之前不能以任何方式执行。一旦验证器完成,就知道了一些重要的属性:
- 操作数堆栈没有溢出或下溢; - 所有字节码指令的参数类型始终是正确的; - 对象字段访问是合法的——私有的、公共的或受保护的。
虽然所有这些检查看起来都非常详细,但是一旦字节码验证器完成其工作,Java解释器就可以继续运行,因为它知道代码将安全地运行。了解这些属性使Java解释器更快,因为它不需要检查任何内容。没有操作数类型检查和堆栈溢出检查。解释器因此可以在不影响可靠性的情况下以全速运行。
从Oracle文档第5.3.2节:
当类加载器L的loadClass方法使用要加载的类或接口C的名称N被调用时,L必须执行以下两个操作之一以加载C:
- 类加载器L可以创建一个表示C的字节数组,作为ClassFile结构的字节(§4.1);然后它必须调用ClassLoader类的defineClass方法。调用defineClass方法将使Java虚拟机使用§5.3.5中找到的算法从字节数组中派生由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 bytecode verification: algorithms and formalizations


6
这是一张简化的图示。实际上,验证并没有在ClassLoader内部发生,因此完全独立于特定的ClassLoader实现。甚至还有其他方法可以将一个类添加到JVM中,例如Instrumentation,但在这些情况下也会验证字节码。同时,“Class Loader”指向“Just in Time Compiler”的箭头没有意义,因为ClassLoader不以任何方式与JIT Compiler交互。相反,您可以认为验证器和JIT已经成为JVM的一个组成部分超过15年了。 - Holger
@TheLostMind 你说得对,这张图片有误导性! - Smrita
@Holger,我的意思是字节码验证器不是ClassLoader的一部分吗? - Smrita
1
@Smrita:ClassLoader 负责定位和加载(或生成)组成类文件的字节。当它将这些字节传递给其中一个 defineClass 方法时,它的责任就结束了。这是 JVM 及其验证器的责任开始的地方。该过程在 JVM 规范 §5.3 中有详细说明。请注意,5.3.2 包含有关 Java1.1 更改(1997 年)的备注。 - Holger
2
那一章节确实是正确的资源。正如我之前的评论所说,§5.3.2包含了有关Java 1.1相关变化的说明。让我引用一下:“从JDK 1.1版本开始,Oracle的Java虚拟机实现直接链接类或接口,而不依赖于类加载器。” - Holger
显示剩余7条评论

9

来自JVM规范4.10

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

接下来会详细说明验证过程。

来自JVM规范5.4.1

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

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

JVM和JLS是很好的文档,当你有这样的问题时可以查阅。


9

没有二次验证

不需要,就验证而言,在下面的图片中仔细观察Java程序如何通过各种阶段,您会发现没有二次验证,但代码只需验证一次。

enter image description here

  • 编辑 - 程序员编写程序(最好在记事本上),并将其保存为“.java”文件,之后由编译器用于编译。
  • 编译 - 编译器接受“.java”文件,编译它,并查找程序范围内的任何可能错误。如果它发现任何错误,则向程序员报告这些错误。如果没有错误,则将程序转换为字节码,并保存为“.class”文件。

  • 加载 - 现在,称为“类加载器”的组件的主要目的是将字节码加载到JVM中。它还没有执行代码,而只是将其加载到JVM的内存中。

  • 验证 - 加载代码后,JVM的子部分Byte Code Verifier检查字节码并验证其真实性。它还检查字节码是否具有可能导致某些恶意结果的代码。 JVM的此组件确保安全性。

  • 执行 - 下一个组件是Execution Engine。 执行引擎使用Just In Time(JIT)编译器逐行解释代码。 JIT编译器执行速度非常快,但会消耗额外的缓存内存。


请参见此链接:https://dev59.com/1XRA5IYBdhLWcg3w_DHF - nobalG

5
规范列出了字节码验证的4个阶段。这些步骤在功能上是不同的,不要误以为是重复的。就像多通道编译器使用每个通道为下一个通道设置一样,阶段不是重复,而是为单个总体目标进行编排,每个阶段完成某些任务。
除非字节码被更改,否则没有必要再次验证它。
验证过程在此处描述。 http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.10

2

代码的验证发生了两次。一次是在编译期间(如果代码存在缺陷、威胁,则编译失败),另一次是在类被加载到内存中后在执行期间实际的字节码验证)。是的,这与通过类加载器加载类的过程同时发生,但类加载器本身可能不会充当验证器。它是JVM(或者更准确地说是JVM中的验证器)进行验证。


那么你的意思是编译器中有能力验证字节码的东西?你能否指出一些资源,让我也可以阅读它呢 :) - Smrita
@Smrita - 请查看这个这个。顺便说一句,我编辑了我的答案以使其更清晰。字节码验证不会发生两次。编译器确保错误的代码总是失败的。因此,这确实是验证,但不是在字节码上。JVM有一个验证器来进行字节码验证 - TheLostMind
现在有点清楚了。所以看起来字节码的验证只会发生一次 :) - Smrita
@Smrita - 是的,看起来是这样。不幸的是,关于这个话题的可用文档要么过时,要么太少。 - TheLostMind

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接