1. 编译后的方法区, 其中存储的代码都是一些字节码指令
2.Java 虚拟机执行模型:
java 代码是在一个线程内部执行, 每个线程都有自己的执行栈, 栈由帧组成, 每个帧表示一个方法的调用, 每调用一个方法, 都将将新的帧压入执行栈, 方法返回时(不管是整成 return 还是异常返回), 该方法对应的帧都将出栈, 即按照先进后出的规则.
执行栈与操作数栈不一样, 操作数栈包含在执行栈中. 每一帧包括局部变量和操作数栈两部分, 操作数栈中包括字节码指令用来当操作数的值. 比如 a.equals(b)将创建一帧, 此时该帧将有一个空栈, 并且 a 和 b 作为局部变量
字节码指令:
由标识该指令的操作码和固定数目的参数组成, 操作码指定要进行哪一类操作, 参数指定具体精确行为. 指令分为两类, 一类在局部变量和操作数栈之间传值, 一类从操作数栈弹出值计算后再压入
例如:
ILOAD,LLOAD,FLOAD,DLOAD,ALOAD 读取一个局部变量, 并将其值压入操作数栈中, 其对应的参数是其读取的局部变量索引 i(因为局部变量就是通过索引来进行随机访问的),LLOAD 和 DLOAD 加载时需要两个槽 (slot), 因为局部变量部分和操作数占部分的每个槽(slot) 都可以保存除了 long 和 double 之外的 java 值(long 和 double 需要两个槽).
ILOAD: 加载 boolean,char,byte,short,int 局部变量
LLOAD: 加载 long
FLOAD: 加载 float
DLOAD: 加载 double
ALOAD: 加载对象和数组引用
对应的 ISTORE,LSTORE,FSTORE,DSTORE,ASTORE 从操作数栈弹出值并将其存储在指定的索引 i 所代表的局部变量中, 所以这些操作指令是和 java 数据类型密切相关的. 存取值和数据类型也相关, 比如使用 ISTORE 1 ALOAD 1, 此时从操作数栈弹出一个 int 值存入索引 1 处的局部变量中, 再将该值转为对象类型进行转换读取是非法的. 但是对于一个局部变量位置, 我们可以在运行过程中改变其类型, 比如 ISTORE 1 ALOAD 1 非法, 但是 ATORE 1 ALOAD1 就合法了. 具体的字节码指令见 ASM 指南附 A.1
通过一个例子来进行学习, 比如以下方法:
- package asm;
- public class bean {
- private int f;
- public bean() {
- }
- public void setF(int f) {
- this.f = f;
- }
- public int getF() {
- return this.f;
- }
- }
直接通过字节码文件查看其 class 文件结构, 其字段就一个 int 类型的 f, 访问修饰符为 private
setf 方法的字节码指令如下
其局部变量表如下, 所以有两个值一个就是当前对象 this 和成员变量 f, 分别对应下标 0 和 1
这里要设计到几个字节码指令:
GETFIELD owner name desc: 读取一个字段的值并将其值压入操作数栈中
PUTFIELD owner name desc: 从操作数弹出值存在 name 所代表的字段中
owner: 类的全限定名
GETSTATIC owner name desc 和 PUTSTATIC owner name desc 类似, 只是为静态变量
aload 0, 读取局部变量 this, 也就是局部变量表下标为 0 处的 this 对象(其在调用这个方法的时候就已经初始化存储在局部变量表中), 然后将其压入操作数栈.
iload 1, 读取局部变量 f, 下标为 1(创建帧期间已经初始化, 也就是入口参数 int f), 压入操作数栈中
putfield #2 <asm/bean.f> 也就是弹出压入的两个值, 赋值给 asm/bean.f, 也就是将入口的 int f 的值赋给 this.f
return 即该方法执行完成, 那么该帧从执行栈从弹出
getf 对应的字节码指令如下所示:
aload 0, 即从局部变量表拿到 this 放入操作数栈
getfield #2 <asm/bean.f> 即从操作数栈中拿出 this, 并将 this.f 的值压入操作数栈
ireturn 返回 f 的值 get 方法的调用者, xreturn,x 即返回变量对应的修饰符
bean 构造方法, 字节码指令如下:
aload 0: 从局部变量表拿到 this, 压入操作数栈
这里要设计方法的调用相关的字节码指令:
INVOKEVIRTUAL owner name desc:
调用 owner 所表示的类的 name 方法
desc 用来描述一个方法的参数类型和返回类型
INVOKESTATIC: 调用静态方法
INVOKESPECIAL: 调用私有方法和构造器
INVOKEINTERFACE: 接口中定义的方法
- invokespecial #1 <java/lang/Object.<init>>: 调用 object 对象的 init 方法, 即 super()调用, 最后 return 返回, 如果是对于以下代码:
- package asm;
- public class bean {
- private int f;
- public void setFf(int f) {
- if(f>0){
- this.f = f;}
- else {
- throw new IllegalArgumentException();
- }
- }
- public int getF() {
- return f;
- }
- }
此时 setf 的字节码指令如下:
iload 1, 从局部表量表中拿出入口参数 int f, 压入操作数栈
ifile 9: 此时弹出操作数栈中的 int f 和 0 进行比较
a. 如果小于等于 0(这里将大于判断转为小于等于的判断), 则到第 12 条指令
new #2 : 新建一个异常对象并压入操作数栈
dup: 重复压入该值一次
invokespecial #4 : 弹出操作栈中两个对象值其中之一, 并调用其构造函数实例化该对象
athrow: 弹出操作数栈中剩下的值(另一个异常对象), 并将其作为异常抛出
b. 如果大于 0, 则依次执行
aload0 从局部变量表拿出 this 对象放入操作数栈中
iload1 拿出入口 int f 的值压入栈中
putfiled #2 <asm/bean.f>: 将 int f 的值赋给 this.f
goto 20: 到第 20 条字节码指令
return : 返回
感觉和汇编有点像, 不过比汇编更容易理解, 主要还是方法内的一些操作, 能看懂基本的字节码指令, 复杂的再去查 doc, 听说面试有时候会问 i++ 和 ++i 的区别:
- package asm;
- public class testplus {
- public void plusf(){
- int i=0;
- System.out.println(i++);
- }
- public void pluse(){
- int i=0 ;
- System.out.println(++i);
- }
- }
编译后:
- //
- // Source code recreated from a .class file by IntelliJ IDEA
- // (powered by Fernflower decompiler)
- //
- package asm;
- public class testplus {
- public testplus() {
- }
- //i++
- public void plusf() {
- int i = 0;
- byte var10001 = i;
- int var2 = i + 1;
- System.out.println(var10001);
- }
- //++i
- public void pluse() {
- int i = 0;
- int i = i + 1;
- System.out.println(i);
- }
- }
首先从生成的 class 来看, i++ 编译后竟然用字节存储了 i 的值, 然后 i 自增 1, 输出的为字节类型 i 即 0, 所以 i++, 最终输出为 0,++i, 直接是 i 自增 1, 然后输出 i, 所以最终输出为 1, 所以 for 循环用 i++, 而不用 ++i
从字节码指令来看:
i++
iconst 0: 首先操作数栈中压入常量 0
istore 1: 然后弹出常量 0 放入局部变量表索引 1 处, 此时局部变量表处 1 处从 i 变为 0, 操作数栈空
getstatic #2 : 即拿到 java.lang.System.out, 即取静态变量 System.out 压入栈中, 此时栈中 1 元素
#2 在常量池中为第二个, 关于该字段的引用说明如下, out 对应的描述符即为 Ljava/io/PrintStream; 那么类类型的描述符就是 L + 类的全限定名 +;
iload 1: 从局部变量表 1 处取值, 压住操作数栈, 即将 0 压入操作数栈
iinc 1 by 1: 给局部变量 1 处的值 + 1, 此时 1 处即从 0 变为 1
invokevirtual: 调用 java.io.PrintStream.println, 此时需要的值是从操作数栈中取的, 然而此时操作数栈顶弹出的数值为 0, 所以输出为 0
++i
iconst 0: 首先操作数栈中压入常量 0
istore 1: 然后弹出常量 0 放入局部变量表索引 1 处, 此时局部变量表处 1 处从 i 变为 0, 操作数栈空
getstatic #2 : 即拿到 java.lang.System.out, 即取静态变量 System.out 压入栈中, 此时栈中 1 元素
iinc 1 by 1: 将局部变量表 1 处的值加 1, 即从 0 变为 1
iload 1: 加载局部变量表 1 处的值, 压入操作数栈中, 即将 1 压入栈中
invokevirtual: 调用 java.io.PrintStream.println, 此时需要的值是从操作数栈中取的, 然而此时操作数栈顶弹出的数值为 1, 所以输出为 1
所以 i++ 和 ++i 的区别从字节码指令上来看就是局部变量表自增和压入操作数栈的顺序不一样, i++ 是先压栈, 后局部变量表自增,++i 是先局部变量表自增, 后压入操作数栈, 这样就完全搞懂了 2333~
所以再分析一个巩固巩固:
- package asm;
- public class testplus {
- public void pluse(){
- int i=0 ;
- int p = 2 + i++ - ++i;
- System.out.println(i);
- System.out.println(p);
- }
- public static void main(String[] args) {
- testplus t = new testplus();
- t.pluse();
- }
- }
main 方法:
new #4 <asm/testplus>:new 一个对象压入栈中
dup: 赋值一个栈顶的对象再压入操作数栈, 关于为什么要压入两个重复的值原因:
首先字节码指令操作数值时基于栈实现的, 那么对于同一个值从栈中操作时必定要弹出, 那么如果对一个数同时操作两次, 那么就要两次压栈. 涉及到 new 一个对象操作时, java 虚拟机自动 dup, 在 new 一个对象以后, 栈中放入的是该对象在堆中的地址, 比如声明以下两个
- class1 a = new class1();
- a.pp()
通常在调用对象调用其类中方法前肯定要调用其 init 实例化, 那么 init 要用一次操作数栈中的地址, 此时弹出一次地址参与方法调用, 后面只需要再将该栈中的地址放入局部变量表, 该地址的对象已经完成了实例化操作, 那么后面每次调用只需要从局部变量表从取到该对象的地址, 即可任意调用其类中的方法.
invokespecial #5 : 这里调用 testplus 的 init 方法, 所以从栈中弹出一个 testplus 的地址
astore 1: 将实例化以后的该 testplus 对象地址放入局部变量表 1 处
aload 1: 取局部变量表 1 处的对象地址压入栈中
invokevirtual #6: 调用 testplus 的 pluse 方法
return : 返回
pluse 方法:
iconst 0: 压入常量 0
istore 1: 弹出 0 存入局部变量表 1 处 (完成 int i=0)
iconst 2: 将 2 压入栈中
iload 1: 取出局部变量表 1 处的值 0 压入栈中
iinc 1 by 1: 局部变量表 1 处的值加 1, 即从 0 变为 1
iadd : 将栈中的两个值相加, 即 stack[0] + stack[1] = 2 + 0 =2
iinc 1 by 1: 局部变量表 1 处的值加 1, 即从 1 变为 2
iload 1: 去局部变量表 1 处的值压入栈中, 即栈顶为 2
isub : 将栈中两个元素相减, 即 stack[0] - stack[1] = 2 - 2 =0
istore 2: 弹出栈中的唯一一个元素 2, 存入局部变量表 2 处, 此时栈空
getstatic # 2 : 拿到 Syetem.out, 压入栈中
iload 1: 取出局部表量表 1 处的值压入栈中, 即栈顶为 2
invokevirtual #3 : 弹出栈中两个元素, 调用 System.out 的 println 方法, 即 stack[0].print(stack[1]), 即输出 2
同理压入 System.out, 然后 iload 2, 取出局部变量表 2 处的 0 压入栈中, 输出 0
最终输出结果也是 2 和 0
来源: https://www.cnblogs.com/tr1ple/p/12790121.html