文章目录
思维导图
01概述
执行模型
字节码与数据类型
指令分析
02-加载与存储指令
复习:再谈操作数栈与局部变量表
1-局部变量压栈指令
举例分析如下:
2-常量入栈指令
举例解释如下:
注意:常量入栈指令中的n和局部变量压栈指令中的n不一样,本次的n代表数值或者对象,而不是局部变量表中的下标
3-出栈入局部变量表指令
举例分析如下:
里面有代码,也有字节码,所以可以根据老师给的图展开分析,
首先该方法被调用的时候,形式参数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>
代表将字符串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类型数据
槽位复用:
注意:在方法没有运行的时候,根据字节码文件就可以计算出需要几个槽位
/**
* @author zhangjianbin
* @date 2021年07月18日21:46
*/
public class LoadAndStoreTest {
//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.常量入栈指令
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 = "hello";
float f = 10.0F;
d = 10;
}
public void foo(long l, float f) {
{
int i = 0;
}
{
String s = "hello";
}
}
}
03-算数指令
对于无穷大和NaN的举例:
所有算术指令
举例
一个曾经的案例1
一个曾经的案例2
注意:println()方法的本地变量表中会放55,这样该方法就可以使用了
比较指令的说明
注意:NaN(Not a Number)
表示不是一个数字,比如0.0/0.0得到的可能是1.0(两个数相等),也可能是0.0(0.0是分子),也可能是无穷大(0.0是分母),所以老师给出的解释是NaN代表无法确定是什么数字,只有double和float类型中可能出现NaN的情况,而long类型不会出现NaN,所以只有lcmp,而没有lcml
/**
* @author zhangjianbin
* @date 2021年07月18日22:29
*/
public class ArithmeticTest {
public void method1() {
int i = 10;
double j = i / 0.0;
System.out.println(j);//无穷大 Infinity
double d1 = 0.00;
double d2 = d1 / 0.0;
System.out.println(d2); //Nan :不是一个确定的数值
}
public void method2() {
float i = 10;
float j = -1;
i = -j;
}
public void method3(int j) {
int i = 100;
i = i + 10;
i += 10;
}
public int method4() {
int a = 80;
int b = 7;
int c = 10;
return (a + b) * c;
}
public int method5(int i, int j) {
return ((i + j - 1) & ~(j - 1));
}
// i++ 和 ++i
public void method6() {
int i = 10;
i++;
//++i;
}
public void method7() {
int i = 10;
int a = i++;
int j = 20;
int b = ++j;
}
public void method8() {
int i = 10;
i = i++;
System.out.println(i);//10
}
}
04-类型转换指令
1-宽化类型转换
2-窄化类型转换
注意:从float、double、long等类型往byte、short、char类型转换的时候,需要先把前面几种类型转换成int类型,然后在从int类型转换到后面这几种类型,所以int类型相等于一种过渡类型
/**
* @author zhangjianbin
* @date 2021年07月19日21:35
*/
public class ClassCastTest {
// 针对于宽化类型转换的基本测试
public void upCast1() {
int i = 10;
long l = i;
float f = i;
double d = i;
float f1 = l;
double d1 = l;
double d2 = f1;
}
// 举例 精度损失的问题
public void upCast2() {
int i = 123123123;
float f = i;
System.out.println(f);
Long l = 1212121212L;
l = 121212121212121212L;
double d = l;
System.out.println(d);
}
// 针对于 byte short等转换为容量大的类型
public void upCast3(byte b) {
int i = b;
long l = b;
double d = b;
}
public void upCast4(short s) {
int i = s;
long l = s;
float f = s;
}
// 窄化类型转换
public void downCast1() {
int i = 10;
byte b = (byte) i;
short s = (short) i;
char c = (char) i;
long l = 10L;
int i1 = (int) l;
byte b1 = (byte) l;
}
public void downCast2() {
float f = 10;
long l = (long) f;
}
public void downCast3() {
short s = 10;
byte b = (byte) s;
}
public void downCast4() {
int i = 128;
byte b = (byte) i;
System.out.println(b);// -128
}
public void downCast5() {
double d1 = Double.NaN; // 表示不确定的值 例如 0.0/0.0
int i = (int) d1;
System.out.println(d1); // NaN
System.out.println(i);//0
double d2 = Double.POSITIVE_INFINITY; // 正无穷大
long l = (long) d2;
System.out.println(l);
System.out.println(Long.MAX_VALUE);
int j = (int) d2;
System.out.println(j);
System.out.println(Integer.MAX_VALUE);
float f = (float) d2;
System.out.println(f);
}
}
05-对象的创建与访问指令
1-创建指令
2-字段访问指令
注意:get***是入栈,而put***是出栈
3-数组操作指令
4-类型检查指令
/**
* @author zhangjianbin
* @date 2021年07月20日20:23
*/
public class NewTest {
// 1 创建指令
public void newInstance() {
Object obj = new Object();
System.out.println(obj);
File file = new File("hello");
}
public void newArray() {
int[] intArray = new int[10];
Object[] objArray = new Object[10];
int[][] mintArray = new int[10][10];
String[][] strArray = new String[10][];
}
//字段访问指令
public void sayHello() {
System.out.println("hello");
}
public void setOrderId() {
Order order = new Order();
order.id = 101;
System.out.println(order.id);
Order.name = "order";
System.out.println(Order.name);
}
private static class Order {
public static String name;
public int id;
}
// 数组操作指令
public void setArray() {
int[] intArray = new int[10];
intArray[3] = 20;
System.out.println(intArray[1]);
boolean[] arr = new boolean[10];
arr[1] = true;
}
public void arrLength() {
double[] arr = new double[10];
System.out.println(arr.length);
}
public String checkCast(Object obj) {
if (obj instanceof String) {
return (String) obj;
} else {
return null;
}
}
}
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-方法返回指令
/**
* @author zhangjianbin
* @date 2021年07月20日21:19
*/
public class MethodInvokeReturnTest {
//方法调用指令 invokespecial 静态分派
public void invoke1() {
// 情况1 类实例构造器方法 <init>()
Date date = new Date();
Thread t1 = new Thread();
// 情况2 父类的方法
super.toString();
// 情况3 私有方法
methodPrivate();
}
private void methodPrivate() {
}
// 方法调用指令 invokestatic 静态分派
public void invoke2() {
methodStatic();
}
public static void methodStatic() {
}
private static void methodStatic2() {
}
// 方法调用指令 invokeinterface
public void invoke3() {
Thread t1 = new Thread();
((Runnable) t1).run();
Comparable<Integer> com = null;
com.compareTo(123);
}
// 方法调用指令 invokeVirtual 动态分派
public void invoke4() {
System.out.println("hello");
Thread t1 = null;
t1.run();
}
// 方法的返回指令
public int returnInt() {
int i = 500;
return i;
}
public double returnDouble() {
return 0.0;
}
public String returnString() {
return "hello";
}
public int[] returnArr() {
return null;
}
public float returnFloat() {
float f = 2.9F;
return f;
}
public byte returnByte() {
return 0;
}
}
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,这个解锁需要在方法退出之前完成,如果方法执行过程中出现了任何异常,将会跳到异常处理的字节码处执行相关代码,如果异常处理的字节码部分出现了问题,那就重新执行异常处理的字节码,这些内容都在异常表中写的很明确,其中异常表也在上面截图中