提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
01-概述
执行模型
字节码与数据类型
指令分析
02-加载与存储指令
复习:再谈操作数栈与局部变量表
1-局部变量压栈指令
举例分析如下:
public void load(int num, Object obj,long count,boolean flag,short[] arr) {
System.out.println(num);
System.out.println(obj);
System.out.println(count);
System.out.println(flag);
System.out.println(arr);
}
这里为了演示方便,局部变量表,用变量名表示的,实际上存储的是实际的值。
2-常量入栈指令
注意:常量入栈指令中的n和局部变量压栈指令中的n不一样,本次的n代表数值或者对象,而不是局部变量表中的下标
public void pushConstLdc() {
int i = -1;
int a = 5;
int b = 6;
int c = 127;
int d = 128;
int e = 32767;
int f = 32768;
}
public void constLdc() {
long a1 = 1;
long a2 = 2;
float b1 = 2;
float b2 = 3;
double c1 = 1;
double c2 = 2;
Date d = null;
}
3-出栈入局部变量表指令
举例分析如下:
public void store(int k, double d) {
int m = k + 2;
long l = 12;
String str = "atguigu";
float f = 10.0F;
d = 10;
}
里面有代码,也有字节码,所以可以根据老师给的图展开分析,首先该方法被调用的时候,形式参数k和d都是有确定的值,由于该方法不是静态方法,所以局部变量表中的第一个位置(槽位)存储this,而第二个位置存储k具体的值,由于老师只是分析,没有调用这个方法,所以老师全部使用的变量名称来代替具体的值,所以明白就好,继续来分析,然后第三个和第四个位置储存d具体的值,由于d是double类型,所以需要占据两个槽位,数据已经准备好了,那就来看字节码,首先iload_1是将局部变量表中下标为1的k值取出来压入操作数栈中,然后iconst_2是将常量池中的整型值2压入操作数栈,iadd让操作数栈弹出的k值和整型值2执行相加操作,之后将相加的结果值m压入操作数栈中,请注意老师的画法,在执行弹栈和压栈操作之后,老师并没有删除操作数栈中的k值和2,这是因为老师让我们知道具体的操作过程,所以故意为之,不过真正的操作是弹栈之后k值和2就会从操作数栈中弹出,之后操作数栈中就没有k值和2了,只有m值了,然后istore_4是将操作数栈中的m值弹出栈,然后放在局部变量表中下标为4的位置,idc2_w #13<12>代表将long型值12压入操作数栈,istore5是将值12弹栈之后放入局部变量表中下标为5的位置,由于12是long型,所以占据两个位置(槽位),ldc #15代表将字符串atguigu压入操作数栈,astore 7代表将字符串atguigu弹栈之后放入局部变量表中下标为7的位置,idc #16<10.0>代表将float类型数据10.0压入操作数栈,fstore 8代表将10.0弹出栈,然后放入局部变量表中下标为8的位置,idc2_w #17<10.0>代表将10.0压入操作数栈,dstore2代表将10.0弹出栈,之后将10.0放入下标为2和3的操作,毕竟这是double类型数据
槽位复用:
public void foo(long l, float f) {
{
int i = 0;
}
{
String s = "Hello, World";
}
}
03-算数指令
所有算术指令
举例
一个曾经的案例1
一个曾经的案例2
注意:println()方法的本地变量表中会放55,这样该方法就可以使用了
i++和++i
代码1:
public void method6(){
int i = 10;
i++;
}
代码1的字节码:
0 bipush 10
2 istore_1
3 iinc 1 by 1
6 return
用javap -v 类名.class 打开字节码文件,找到method 6 ()方法:
public void method6();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: bipush 10
2: istore_1
3: iinc 1, 1
6: return
LineNumberTable:
line 48: 0
line 49: 3
line 53: 6
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/atguigu/java/ArithmeticTest;
3 4 1 i I
我们一行行解析字节码:
-
0 bipush 10 :把 10 放进操作数栈中
-
istore_1:取出操作数栈中的10 放到局部变量表索引为1的位置(因为这个是非静态的方法,局部变量表索引为0 的位置放的是this)
-
iinc 1 by 1:把局部变量表中索引为1的位置加1。
-
所以这行指令执行完之后,这个索引为1的位置就是11了。 -
return:因为是void类型的方法,因此这里直接return (注意void方法也是有返回指令的)
因此代码1 如果打印i的话:
public void method6(){
int i = 10;
i++;
System.out.println(i);//10
}
0 bipush 10
2 istore_1
3 iinc 1 by 1
6 getstatic #2 <java/lang/System.out>
9 iload_1
10 invokevirtual #5 <java/io/PrintStream.println>
13 return
6 getstatic #2 <java/lang/System.out>:System.out获取静态变量System.out
9 iload_1:把局部变量表的索引为1的【10】放到操作数栈中
10 invokevirtual #5 <java/io/PrintStream.println>:调用println方法
因此输出结果就是10.
代码2:
用与代码1同样的方式分析,我这里就不像代码1那么具体了,方法是一样的。
public void method8(){
int i = 10;
i = i++;
}
代码2的字节码
0 bipush 10
2 istore_1
3 iinc 1 by 1
6 return
你会发现,它与代码1的字节码是一样的!这里仅仅是因为,我们没有只是简单的i++和++i,而没有进行其它的操作,所以,本质山i++和++i如果不做其他的赋值操作的话,这俩其实是效果等价的
这也就解释了 for(int i =0;i<10;i++) 和 for(int i =0;i<10;++i)其实是一样的
所以这个i如果打印一下,其实也是在索引为1的取索引为1的位置上的【10】,这里我们就不再演示了,感兴趣的同学,可以验证一下。
代码 3:
public void method7(){
int i = 10;
int a = i++;
int j = 20;
int b = ++j;
}
0 bipush 10
2 istore_1
3 iload_1
4 iinc 1 by 1
7 istore_2
8 bipush 20
10 istore_3
11 iinc 3 by 1
14 iload_3
15 istore 4
17 return
public void method7();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1
7: istore_2
8: bipush 20
10: istore_3
11: iinc 3, 1
14: iload_3
15: istore 4
17: return
LineNumberTable:
line 55: 0
line 56: 3
line 58: 8
line 59: 11
line 60: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 this Lcom/atguigu/java/ArithmeticTest;
3 15 1 i I
8 10 2 a I
11 7 3 j I
17 1 4 b I
-
0 bipush 10
-
2 istore_1:
-
3 iload_1:
-
4 iinc 1 by 1
-
7 istore_2
-
8 bipush 20
- 10 istore_3
- 11 iinc 3 by 1
- 14 iload_3
- 15 istore 4
- 17 return 方法结束
最终我们局部变量表是这样子的:
把结果输出一下看看啥样子
public void method7(){
int i = 10;
int a = i++;
int j = 20;
int b = ++j;
System.out.println(a);
System.out.println(i);
System.out.println(b);
System.out.println(j);
}
结果
10
11
21
21
字节码:
0 bipush 10
2 istore_1
3 iload_1
4 iinc 1 by 1
7 istore_2
8 bipush 20
10 istore_3
11 iinc 3 by 1
14 iload_3
15 istore 4 //0到15 与上面一样,下面才是表示输出的字节码
17 getstatic #2 <java/lang/System.out>
20 iload_2
21 invokevirtual #5 <java/io/PrintStream.println>
24 getstatic #2 <java/lang/System.out>
27 iload_1
28 invokevirtual #5 <java/io/PrintStream.println>
31 getstatic #2 <java/lang/System.out>
34 iload 4
36 invokevirtual #5 <java/io/PrintStream.println>
39 getstatic #2 <java/lang/System.out>
42 iload_3
43 invokevirtual #5 <java/io/PrintStream.println>
46 return
有字节码分析可以知道
- System.out.println(a)的字节码:
17 getstatic #2 <java/lang/System.out>
20 iload_2
21 invokevirtual #5 <java/io/PrintStream.println>
往操作数栈加载是上图索引为2的【10】。所以a输出的结果是10
-
System.out.println(i);
24 getstatic #2 <java/lang/System.out>
27 iload_1
28 invokevirtual #5 <java/io/PrintStream.println>往操作数栈加载是上图索引为1的【11】。所以i输出的结果是11
-
System.out.println(b);
31 getstatic #2 <java/lang/System.out>
34 iload 4
36 invokevirtual #5 <java/io/PrintStream.println>往操作数栈加载是上图索引为4的【21】。所以i输出的结果是21
-
System.out.println(j);
39 getstatic #2 <java/lang/System.out>
42 iload_3
43 invokevirtual #5 <java/io/PrintStream.println>往操作数栈加载是上图索引为3的【21】。所以i输出的结果是21
所以现在你应该体会到了为什么说i++是先赋值再加加,而++i 是先加加再赋值了吧,一定要注意这个口令不是对所有的场景都适用,比如我们一开始举的例子代码1和代码2。
下面来点有意思的:
代码4:
public void method8(){
int i = 10;
i = i++;
System.out.println(i);
}
这个i打印是多少呢?答案是10
字节码指令:
0 bipush 10
2 istore_1
3 iload_1
4 iinc 1 by 1
7 istore_1
8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #5 <java/io/PrintStream.println>
15 return
这个我就不画图针对字节码一行一行的解释了。
这里面istore_1两次,后面操作栈中的10把之前局部变量表的11覆盖了,所以打印就是10了,你可以按照我上面的方法,尝试画一下流程,授人以鱼不如授人以渔。
再改一下上面代码:
代码5:
public void method8(){
int i = 10;
i = ++i;
System.out.println(i);//11
}
0 bipush 10
2 istore_1
3 iinc 1 by 1
6 iload_1
7 istore_1
8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #5 <java/io/PrintStream.println>
15 return
这个i打印出来就是11
对比一下上面两个字节码,其实最大的不同在于:
代码4是:
3 iload_1
4 iinc 1 by 1
而代码5是:
3 iinc 1 by 1
6 iload_1
这就造成了代码4 加载进操作数栈的是10,代码5 是11,这就造成了再次istore_1时,前者覆盖是10覆盖11,后者是11覆盖11。
比较指令
注意:NaN(Not a Number)表示不是一个数字,比如0.0/0.0得到的可能是1.0(两个数相等),也可能是0.0(0.0是分子),也可能是无穷大(0.0是分母),所以老师给出的解释是NaN代表无法确定是什么数字,只有double和float类型中可能出现NaN的情况,而long类型不会出现NaN,所以只有lcmp,而没有lcml
04-类型转换指令
1-宽化类型转换
2-窄化类型转换
注意:从float、double、long等类型往byte、short、char类型转换的时候,需要先把前面几种类型转换成int类型,然后在从int类型转换到后面这几种类型,所以int类型相等于一种过渡类型
05-对象的创建与访问指令
1-创建指令
2-字段访问指令
注意:get是入栈,而put是出栈
举例:
3-数组操作指令
4-类型检查指令
06-方法调用与返回指令
1-方法调用指令
注意:
1、invokedynamic老师不讲,估计是很少遇到吧
2、invokeinterface是对接口而言的,用属于接口类型的对象调用方法的时候就是这个
3、invokespecial只有构造器、私有方法、super.方法名()调用父类方法这几种情况,其中调用父类方法这种情况可能出现其直接父类没有该方法,那就可以调用其父类继承的父类中的该方法,最终找到一个方法调用就是了
4、invokestatic是调用static静态方法,无论是使用对象.静态方法名()还是类名.静态方法名()都是invokestatic,也不难理解
5、invokevirtual是调用类中的非静态普通方法,而这种实例方法可能调用的是子类重写的非静态普通方法,比如A a = new B();a.hello(),其中B类继承A类,并且B类重写了A类中的hello()方法,这种情况下就是invokevirtual了,但是有可能该类没有子类,调用的就是本类中的非静态普通方法,这种情况也是invokevirtual了
2-方法返回指令
07-操作数栈管理指令
08-控制转义指令
1-条件跳转指令
注意:
1、对于float、double、long类型的比较,它们比较之后生成的是int类型的0、1、-1,这个过程可以使用比较指令和条件跳转指令来完成,虽然得到的是int类型的值,但是System.out.println(XXX)中的值是布尔类型,你可以在jclasslib中的常量池信息中看到写的是Z,代表布尔值类型
2、int类型值(包含byte、char、short)比较 和 对象类型值比较需要使用比较条件跳转指令
2-比较条件跳转指令
注意:
1、上面所说的后者是栈顶元素,而前者是栈顶下面的元素
2、对于float、double、long类型的比较,它们比较之后生成的是int类型的0、1、-1,这个过程可以使用比较指令和条件跳转指令来完成
而 int类型值(包含byte、char、short)比较 和 对象类型值比较需要使用比较条件跳转指令,其中对象类型值不是比较的地址,就是比较对象中的某些字段值,这又归咎到float、double、long、int类型的比较中了
3、无论哪种比较,也不管两个比较值的中间是什么符号(>、<、>=、<=等等),始终都是栈顶下部元素 比较符 栈顶元素,这是不会改变的,然后结合比较符得出结果,如果是true,那就跳转,否则不跳转继续往下执行
3-多条件分支跳转
4-无条件跳转
09-异常处理指令
1-抛出异常指令
注意:
如果使用throw new 异常名称()这种形式来抛出异常,那就会在代码中出现athrow指令,而在方法上面添加throw 异常名称这种形式来抛出异常,然后使用jclasslib的时候就会出现在方法下面多出现一个属性Exceptions,如下图所示:
2-异常处理与异常表
异常表如下所示:
异常表的含义是如果在Start PC和End PC之间(大于等于Start PC,小于End PC)出现对应的Catch Type异常问题(出现异常就匹配对应的异常),将会在操作数栈中压入相应的异常类对象,之后跳转到Handler PC的位置去执行对应的字节码指令
注意:
当异常出现的时候也会压入操作数栈,之后还会存储局部变量表中
10-同步控制指令
1-方法级的同步
同步方法(添加synchronized的方法):
注意:
一个方法无论是否添加synchronized,你都无法在字节码中看出区别,例如:
是否是同步方法在字节码文件中你是无法看出区别的,但是可以在方法访问标识中看出区别
2-方法内指令指令序列的同步
同步代码块:
例子:
操作数栈中的对象和monitorenter结合起来可以让线程获取锁,做法就是让对象的监视器标记从0变成1,这就代表该线程上锁了,然后在操作数栈的aload_1和monitorexit结合起来就可以让线程解锁,做法就是让对象的监视器标记从1变成0,这个解锁需要在方法退出之前完成,如果方法执行过程中出现了任何异常,将会跳到异常处理的字节码处执行相关代码,如果异常处理的字节码部分出现了问题,那就重新执行异常处理的字节码,这些内容都在异常表中写的很明确,其中异常表也在上面截图中