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

Smr*_*ita 30 java verification jvm

所以我对JVM中发生的字节码验证有点困惑.根据Deitel和Deitel的书,Java程序经历了五个阶段(编辑,编译,加载,验证和执行)(第1章).字节码验证器在"验证"阶段验证字节码.书中没有提到字节码验证器是类加载器的一部分.

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

现在,是Deitel和Deitel谈到的字节码验证,以及这个oracle文件 谈到的字节码验证, 同样的过程?

或者字节码验证是否发生两次,一次是在链接过程中,另一次是字节码验证器?

图片描述了Dietel和Dietel在书中提到的java程序的各个阶段.(我从nobalG下面的一个答案中借用了这张图片:)) 在此输入图像描述

Rah*_*thi 21

您可以使用此图表了解字节代码验证,这在Oracle文档中有详细说明

在此输入图像描述

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

图显示数据和控制的从Java语言源代码通过Java编译器的流动,对类加载器和字节码校验,因而对于Java虚拟机,它包含了解释和运行时系统.重要的问题是Java类加载器和字节码验证器没有对字节码流的主要来源做出任何假设 - 代码可能来自本地系统,或者它可能已经绕过地球一半.字节码验证器充当一种网守:它确保传递给Java解释器的代码处于适合的状态以便执行并且可以运行而不用担心破坏Java解释器.在通过验证程序的测试之前,不允许以任何方式执行导入的代码.完成验证程序后,会知道许多重要属性:

  • 没有操作数堆栈溢出或下溢
  • 已知所有字节码指令的参数类型总是正确的
  • 已知对象字段访问是合法的 - 私有,公共或受保护

虽然所有这些检查看起来都非常详细,但是当字节码验证器完成其工作时,Java解释器可以继续进行,因为知道代码将安全运行.了解这些属性会使Java解释器更快,因为它不需要检查任何内容.没有操作数类型检查和堆栈溢出检查.因此,解释器可以全速运行而不会影响可靠性.

编辑:-

来自Oracle Docs Section 5.3.2:

当使用要加载的类或接口C的名称N调用类加载器L的loadClass方法时,L必须执行以下两个操作之一才能加载C:

  • 类加载器L可以创建一个字节数组,表示C作为ClassFile结构的字节(§4.1); 然后它必须调用类ClassLoader的方法defineClass.调用defineClass会导致Java虚拟机使用§5.3.5中的算法从字节数组中使用L来导出由N表示的类或接口.
  • 类加载器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;
}
Run Code Online (Sandbox Code Playgroud)

相应的字节代码将是

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
Run Code Online (Sandbox Code Playgroud)

请注意,JVM中的大多数指令都是键入的.

现在您应该注意,除非代码至少满足以下条件,否则无法保证JVM的正常运行:

  • 类型正确性:指令的参数始终是指令所期望的类型.
  • 没有堆栈溢出或流动:指令永远不会弹出空堆栈的参数,也不会将结果推送到完整堆栈(其大小等于为方法声明的最大堆栈大小).
  • 代码遏制:程序计数器必须总是用于该方法的代码内指向,一个有效的指令编码的开始(没有落下øFF的方法代码的末尾;无分支成的指令编码的中间).
  • 寄存器初始化:来自寄存器的负载必须始终跟在该寄存器中的至少一个存储器中; 换句话说,在方法入口处不会初始化与方法参数不对应的寄存器,从未初始化的寄存器加载是错误的.
  • 对象初始化:当创建类C的实例时,必须在可以使用类实例之前调用类C的初始化方法之一(对应于此类的构造函数).

字节代码验证的目的是通过在加载时对字节代码进行静态分析来一劳永逸地检查这些条件.然后可以更快地执行通过验证的字节代码.

还要注意,字节码验证的目的是将上面列出的验证从运行时转移到加载时.

上面的解释取自Java字节码验证:算法和形式化

  • @TheLostMind:这是一个简化的插图.实际上,验证不会在`ClassLoader`中发生,因此完全独立于特定的`ClassLoader`实现.甚至还有其他方法可以将类添加到JVM,例如Instrumentation,但在这些情况下也会验证字节代码.此外,从"类加载器"到"即时编译器"的箭头没有任何意义,因为"ClassLoader"不会以任何方式与JIT编译器交互.相反,您可以将验证者和JIT视为JVM的一个组成部分已超过十五年了. (6认同)
  • 那一章*是*适当的资源.正如我之前的评论中所述,§5.3.2包含有关Java 1.1相关更改的注释.让我引用:"*从JDK 1.1版开始,Oracle的Java虚拟机实现直接链接类或接口,而不依赖于类加载器.*" (2认同)

jdp*_*nix 9

没有.

JVM规范4.10:

尽管Java编程语言只能产生满足在前面章节中的所有静态和结构限制类文件的编译器,Java虚拟机,它被要求加载任何文件被由编译器生成或正确难保形成.

然后继续指定验证过程.

JVM Spec 5.4.1:

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

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

当您遇到类似问题时,JVM和JLS是很棒的文档.


nob*_*alG 9

没有这样两次验证

,就验证而言,仔细查看用java编写的程序如何经历下图中的各个阶段,您将看到没有这样的两次验证,但代码只验证一次.

在此输入图像描述

  • 编辑 - 程序员编写程序(最好在记事本上)并将其保存为'.java'文件,然后由编译器进一步用于编译.
  • COMPILE - 这里的编译器接受'.java'文件,编译它并查找程序范围内的任何可能的错误.如果发现任何错误,它会将它们报告给程序员.如果没有错误,则程序将转换为字节码并保存为".class"文件.

  • LOAD - 现在称为"类加载器"的组件的主要目的是在JVM中加载字节代码.它尚未执行代码,只是将其加载到JVM的内存中.

  • 验证 - 加载代码后,JVM的子代码"Byte Code verifier"检查字节码并验证其真实性.它还检查字节码是否具有可能导致某些恶意结果的任何此类代码.JVM的这个组件可确保安全性.

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

  • 这是Dietel和Dietel中提到的图表.没有任何地方谈论字节码验证器是类加载器的一部分!!即使这个图表也不清楚.这个图表是我混淆的主要原因!! (2认同)

cod*_*eim 5

该规范列出了字节码验证中的4个阶段.这些步骤在功能上是截然不同的,不要错过重复相同的事情.就像多次传递编译器使用每个传递来设置下一次传递一样,阶段不是重复,而是针对单个总体目的进行编排,每个阶段完成某些任务.

除非更改字节码,否则没有理由对其进行两次验证.

此处描述了验证.

http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.10