问题:
JVM中的字节码是如何执行的?
一、虚拟机常见的实现方式有两种:Stack based (基于栈)和 Register based(基于寄存器)
1)Stack based (基于栈)
Hotspot JVM是基于栈实现的;
public static int add(int a,int b) { return a + b; } code: 0 iload_0 // 将a压入操作数栈中 1 iload_1 // 将b压入操作数栈中 2 iadd // 将栈顶两个值出栈,相加,然后将结果放回栈顶 3 ireturn // 将栈顶作为结果返回
2)Register based(基于寄存器)
基于寄存器的 LuaVM 的 lua 源码和字节码如下,查看字节码使用luac -l -l -v -s test.lua 命令
源码 local function my_add(a, b) return a + b; end 对应字节码 1 [3] ADD 2 0 1 基于寄存器的 add 指令直接把寄存器 R0 和 R1 相加,结果保存在寄存器 R2 中。
两者有什么不同呢?
基于栈和寄存器的指令集各有优缺点,基于栈的指令集移植性更好,代码更加紧凑、编译器实现更加简单,但完成相同功能所需的指令数一般比寄存器架构多,需要频繁的入栈出栈,栈架构指令集的执行速度会相对而言慢一些。
二、Hotspot JVM 是一个基于栈的虚拟机,每个线程都有一个虚拟机栈,存储了「栈帧」。每次方法调用都伴随着栈帧的创建销毁。
1)栈帧
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构 栈帧随着方法调用而创建,随着方法结束而销毁,
栈帧的存储空间分配在 Java 虚拟机栈中,每个栈帧拥有自己的局部变量表(Local Variables)、操作数栈(Operand Stack) 和 指向运行时常量池的引用
2)局部变量表
每个栈帧内部都包含一组称为局部变量表(Local Variables)的变量列表,局部变量表的大小在编译期间就已经确定。 Java 虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用时,它的参数会被传递到从 0 开始的连续局部变量列表位置上。 当一个实例方法(非静态方法)被调用时,第 0 个局部变量是调用这个实例方法的对象的引用(也就是我们所说的 this )
3)操作数栈
每个栈帧内部都包含了一个称为操作数栈的后进先出(LIFO)栈,栈的大小同样也是在编译期间确定。 Java 虚拟机提供的一些字节码指令用来从局部变量表或者对象实例的字段中复制常量或者变量到操作数栈,也有一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用时,操作数栈也用来准备调用方法的参数和接收方法返回的结果。 比如 iadd 指令用来将两个 int 类型的数值相加,它要求执行之前操作数栈已经存在两个由前面其它指令放入的 int 型数值,在 iadd 指令执行时,两个 int 值从操作数栈中出栈,相加求和,然后将求和的结果重新入栈。 整个 JVM 指令执行的过程就是局部变量表与操作数栈之间不断 load、store 的过程
4)从二进制看 class 文件和字节码
我们可以手动用 16 进制编辑器去修改这些字节码文件,只是比较容易出错,所以产生了一些字节码操作的工具,最出名的莫过于 ASM 和 Javassist。