JVM 字节码(字节码指令 和 操作数栈、常量池的关系)
字节码指令
构造方法 的字节码指令
2a b7 00 01 b1
那这些具体代表着什么含义?我们可以在官方文档当中进行查看不同的数值代替什么不同的含义:以2a为例,在这里都是十六进制的数,在文档当中查找0x2a,其余指令同理:官网文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html
- 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
- b7 => invokespecial 预备调用构造方法,哪个方法呢?
- 00 01 引用常量池中 #1 项,即
【 Method java/lang/Object."<init>":()V 】
构造方法,V表示无返回值 - b1 表示返回
第二个就是主方法的字节码指令 public static void main(java.lang.String[]);
b2 00 02 12 03 b6 00 04 b1
和上方同理,在官网文档当中查找字节码信息
- b2 => getstatic 用来加载静态变量,哪个静态变量呢?
- 00 02 引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】
- 12 => ldc 加载参数,哪个参数呢?
- 03 引用常量池中 #3 项,即 【String hello world】
- b6 => invokevirtual 预备调用成员方法,哪个方法呢?
- 00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
- b1 表示返回
javap工具
对class文件进行反编译,使用命令javap -v class文件名
字节码指令 和 操作数栈、常量池的关系
使用一段简单的java代码进行测试
public class relationship {
public static void main(String[] args) {
int a= 10;
int b=Short.MAX_VALUE+1;
int c= a+b;
System.out.print(c);
}
}
运行java代码得到class文件,使用javap指令对class文件进行反编译,在这里第三行的32768这个数只,在这里只有Short所含的数字大小才会跟随变量进行保存,其余大的数值都会保存在常量池当中。
随后会将常量池载入到运行时常量池
方法字节码载入方法区
main 线程开始运行,分配栈帧内存
到这里就运行main方法当中的指令,在对class反编译后,查看对应的code
1 :bipush 10
- 将一个 byte 压入操作数栈(其长度会补齐 4 个字节)
- sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
- 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
2: istore_1
将操作数栈顶数据弹出,存入局部变量表的 slot 1
3: ldc #3
从常量池加载 #3 数据到操作数栈
Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的
4 : istore_2
5:iload_1 和 iload_2
读取数据到操作栈上
6:iadd
执行相加算法
7:istore_3
8:getstatic #4
在常量池当中找到成员变量的引用。
读取之后放入操作栈当中。
9:iload3
读取第3个栈当中的数据
10:invokevirtual #5
- 找到常量池 #5 项
- 定位到方法区 java/io/PrintStream.println:(I)V 方法
- 生成新的栈帧(分配 locals、stack等)
- 传递参数,执行新栈帧中的字节码
执行完毕,弹出栈帧,清除 main 操作数栈内容
return :完成 main 方法调用,弹出 main 栈帧,程序结束。
案例分析 a- -
一个简单的代码段进行测试:使用字节码的技术对这段代码进行分析:
public class interview {
public static void main(String[] args) {
int a=10;
int b = a++ + ++a + a--;
System.out.print(a);
System.out.print(b);
}
}
还是一样,先使用javap对class文件进行反编译,其中:
- iinc 指令是直接在局部变量 slot 上进行运算
- a++ 是先执行 iload 还是 先执行 iinc
- ++a 是先执行 iinc 还是 先执行 iload
- bipush 10 把10放进操作栈当中
- istore_1 把10弹出,放进第一个栈帧当中
- iload_1 把第一个栈帧的数据读取出来放入操作栈当中
- linc 进行自增
- linc load先自增再load a=12
- 先load再自减
- 最后弹出数据的结果 a=11 b=34
条件判断指令
如下表所示:
- byte , short , char都会按int比较,因为操作数栈都是4字节
- goto用来进行跳转到指定行号的字节码
使用一个简单的判断的java代码进行测试
public class if_demo {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
a = 20;
} else {
a = 10;
}
}
}
之后还是使用javap对class文件进行反编译:
循环指令控制
使用一段简单的java循环代码进行测试,在这里while循环和do while同理。
public class while_demo {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}
使用javap对class进行反编译,查看code编码
案例分析(循环)
查看以下代码段:判断x最后的值
public class case_analysis {
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
System.out.println(x);
}
}
答案是0,在这里的x=x++;这条命令在字节码当中分为了两个阶段,先进行load在进行ilic,可以发现,在这里先把x的值加载到操作栈当中,而在栈帧当中进行自加,再把操作栈当中的x值覆盖栈帧当中的值,也就是一直是把x=0这个值对栈帧当中自增的值进行覆盖,所以x始终为0;