zl程序教程

您现在的位置是:首页 >  Python

当前栏目

12 虚拟机字节码执行引擎_基于栈的字节码解释执行

2023-04-18 15:24:43 时间

1 解释执行与编译执行

  1. 解释执行:通过解释器执行
  2. 编译执行:通过即时编译器产生本地代码(机器码)执行
  3. 虚拟机的执行引擎支持以上两种方式

java语言的编译及执行过程:

C语言的编译过程:

2 基于栈/寄存器的指令集

  1. 基于栈的指令集架构:指令不带参数,使用操作数栈中的数据作为指令的运算输入,指令的运算结果也存储在操作数栈中。依赖操作数栈进行工作。字节码指令属于基于栈的指令集架构
  2. 基于寄存器的指令集架构:能被物理硬件直接支持的指令集架构,如X86二进制指令集,指令带有操作数依赖寄存器计算和存储。(所有主流物理机的指令集都是寄存器架构)

温馨提示:关于x86指令集在《深入理解计算机操作系统》读书笔记系列中有详细介绍

以计算1+1作为例子,体现两种指令集的差异

基于栈的指令集:

iconst_1  
iconst_1 
iadd 
istore_0
  1. 两条iconst_1指令连续把两个常量1压入栈
  2. iadd指令把栈顶的两个值出栈、相加,然后把结果放回栈顶
  3. 最后istore_0把栈顶的值放到局部变量表的第0个变量槽中

基于寄存器的指令集:

mov eax, 1 
add eax, 1
  1. mov指令将立即数1放入eax寄存器
  2. add指令将eax寄存器的值加1,并把结果保存在eax寄存器中

栈的指令集的优缺点:

  1. 可移植,因为它不依赖寄存器(寄存器跟硬件体系挂钩)
  2. 指令相对更加简洁(寄存器指令集中还需要存放参数)
  3. 编译器实现更加简单(不需要考虑空间分配的问题,所需空间都在栈上操作)
  4. 缺点1:栈实现在内存中,比起寄存器的访存速度更慢。
  5. 缺点2:指令数量比寄存器架构来得更多(出栈、入栈操作本身就产生了相当大量的指令)

3 解释器执行字节码过程

例子说明Java虚拟机如何执行字节码

public class ByteCodeTest_3 {
    public int calc() {
        int a = 100;
        int b = 200;
        int c = 300;
        return (a + b) * c;
    }
}

javac编译后的字节码完整内容如下:

minnesota-book:test howing.zhang$ javap -verbose ./ByteCodeTest_3.class
Classfile /Users/howing.zhang/hy-Data/Work/study/project/mconcurrency/src/main/java/com/minnesota/practice/test/ByteCodeTest_3.class
  Last modified 2022-12-6; size 310 bytes
  MD5 checksum 542ed788c7ea7a11f0c7cd0cf046c2a6
  Compiled from "ByteCodeTest_3.java"
public class com.minnesota.practice.test.ByteCodeTest_3
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#12         // java/lang/Object."<init>":()V
   #2 = Class              #13            // com/minnesota/practice/test/ByteCodeTest_3
   #3 = Class              #14            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               calc
   #9 = Utf8               ()I
  #10 = Utf8               SourceFile
  #11 = Utf8               ByteCodeTest_3.java
  #12 = NameAndType        #4:#5          // "<init>":()V
  #13 = Utf8               com/minnesota/practice/test/ByteCodeTest_3
  #14 = Utf8               java/lang/Object
{
  public com.minnesota.practice.test.ByteCodeTest_3();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public int calc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        100
         2: istore_1
         3: sipush        200
         6: istore_2
         7: sipush        300
        10: istore_3
        11: iload_1
        12: iload_2
        13: iadd
        14: iload_3
        15: imul
        16: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 3
        line 7: 7
        line 8: 11
}
SourceFile: "ByteCodeTest_3.java"

用图例来说明执行calc()方法的字节码指令时,操作数栈和局部变量表的变化情况:

由上图可知,calc()方法运行的栈帧结构中:

  1. 操作数栈的最大深度为2
  2. 本地变量表的变量槽最大使用数量为4
  3. 方法参数的个数为1,它是隐藏参数this
  4. 验证过程也满足:“stack=2, locals=4, args_size=1”该字节码内容

4 栈结构指令集的一般运行过程

java虚拟机进行数据运算,没有异常的情况下,遵从以下步骤:

  1. 先入栈:将常量或变量值写入操作数栈,push指令集负责。
  2. 出栈:将操作数栈数据保存到常量表,store指令集负责
  3. 入栈:数据从常量表复制操作数栈,为下一步的运算进行准备。load指令集负责
  4. 运算:对栈内(从栈顶开始寻找)数据进行运算,并把结果重新存入栈顶。运算指令集负责
  5. 返回:把计算结果(栈顶数据)返回给方法调用者。

5 总结

一、虚拟机是如何执行方法里面的字节码指令?

  1. 首先java代码被编译成字节码,字节码中含有方法执行需要的全部指令
  2. java虚拟机基于栈帧(前面章节介绍过栈结构)的数据结构,来执行指令
  3. 指令执行过程,就是在操作数栈和局部变量表中来回传送数据的过程
  4. 传送数据的一般过程,参考【栈结构指令集的一般运行过程】

二、虚拟机实际如何执行字节码指令?

是动态产生每条字节码对应的汇编代码来运行。

三、回到开头:解释执行,那么解释器在哪里?解释器怎么工作?
todo