JVM第十二篇(类加载与字节码技术三)

指令

条件判断指令
在这里插入图片描述
byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
goto 用来进行跳转到指定行号的字节码

public class Demo3_3 {
	public static void main(String[] args) {
		int a = 0;
		if(a == 0) {
			a = 10;
		} else {
			a = 20;
		}
	}
}

代码对应的指令

0: iconst_0		//将0放入操作数栈
1: istore_1		//0存放到1号槽位
2: iload_1		//将1号槽位加载到操作数栈
3: ifne 12		//判断操作数栈中的值不等于0,如果不等于0跳转到12行 
6: bipush 10	// 将10放入操作数栈
8: istore_1		// 将操作数栈放入1号槽位
9: goto 15		// 跳转到 15行
12: bipush 20	// 将20放入操作数栈
14: istore_1	// 将操作数栈放入1号槽位
15: return		// 结束,返回

循环控制指令
其实循环控制还是前面介绍的那些指令

while 循环

public class Demo3_4 {
	public static void main(String[] args) {
		int a = 0;
		while (a < 10) {
			a++;
		}
	}
}
0: iconst_0		//将0放入操作数栈
1: istore_1		// 0存放入1号槽位
2: iload_1		// 将1号槽位的值加载入操作数栈
3: bipush 10	// 将10放入操作数栈
5: if_icmpge 14	// 比较是否大于等于 是则跳14行 
8: iinc 1, 1	// 将1号槽位值加1
11: goto 2	    // 跳第2行
14: return

do while 循环:

public class Demo3_5 {
	public static void main(String[] args) {
		int a = 0;
		do {
			a++;
		} while (a < 10);
	}
}
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iload_1
6: bipush 10
8: if_icmplt 2
11: return

for循环

public class Demo3_6 {
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
		}
	}
}
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return

while 和 for 的字节码,是一模一样的。

方法

构造方法
1) cinit()V

public class Demo3_8_1 {
	static int i = 10;
	static {
		i = 20;
	} 
	static {
		i = 30;
	}
}

编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 cinit()V 。

代码对应的字节码。cinit()V 方法会在类加载的初始化阶段被调用

0: bipush 10
2: putstatic #2 // Field i:I
5: bipush 20
7: putstatic #2 // Field i:I
10: bipush 30
12: putstatic #2 // Field i:I
15: return

init ()V

public class Demo3_8_2 {
	private String a = "s1";
	{
		b = 20;
	} 
	private int b = 10;
	{
		a = "s2";
	}
	public Demo3_8_2(String a, int b) {
		this.a = a;
		this.b = b;
	} 
	public static void main(String[] args) {
		Demo3_8_2 d = new Demo3_8_2("s3", 30);
		System.out.println(d.a);
		System.out.println(d.b);
	}
}

对应的字节码指令。
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后

0: aload_0
1: invokespecial #1 	// super.<init>()V
4: aload_0
5: ldc #2 				// <- "s1"
7: putfield #3			// -> this.a
10: aload_0
11: bipush 20 			// <- 20
13: putfield #4 		// -> this.b
16: aload_0
17: bipush 10 			// <- 10
19: putfield #4 		// -> this.b
22: aload_0
23: ldc #5 				// <- "s2"
25: putfield #3 		// -> this.a
28: aload_0 			// ------------------------------
29: aload_1 			// <- slot 1(a) "s3" 	|
30: putfield #3 		// -> this.a 			|
33: aload_0 			//						|
34: iload_2 			// <- slot 2(b) 30 		|
35: putfield #4 		// -> this.b --------------------
38: return

方法调用:
看一下几种不同的方法调用对应的字节码指令

public class Demo3_9 {
	public Demo3_9() { }
	private void test1() { }
	private final void test2() { }
	public void test3() { }
	public static void test4() { }
	public static void main(String[] args) {
		Demo3_9 d = new Demo3_9();
		d.test1();
		d.test2();
		d.test3();
		d.test4();
		Demo3_9.test4();
	}
}

对应的字节码指令

0: new #2 				// class cn/itcast/jvm/t3/bytecode/Demo3_9
3: dup
4: invokespecial #3 	// Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 	// Method test1:()V
12: aload_1
13: invokespecial #5	// Method test2:()V
16: aload_1
17: invokevirtual #6 	// Method test3:()V
20: aload_1
21: pop
22: invokestatic #7 	// Method test4:()V
25: invokestatic #7 	// Method test4:()V
28: return

最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静
态绑定。
普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态。即 test3:(),在编译期间,不能确定要调用的方法,需要在执行期间确定,称为动态绑定。
成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】。
new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈,dup 是复制操作数栈栈顶的内容,本例即为【对象引用】,执行完毕后,此时操作数栈中有两个对象引用。invokespecial #3,调用当前操作数栈顶的构造方法,执行完毕之后,栈顶的对象引用清除。 astore_1将对象的引用赋值给变量表中的1号槽位。
比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用
invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了
20: aload_1
21: pop
这说明静态方法不需要通过对象引用来调用。使用对象引用来调用静态方法,会在字节码中生成两条无用的指令,效率变低。

多态的原理

java 代码示例

package cn.itcast.jvm.t3.bytecode;
import java.io.IOException;
/**
* 演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩
* -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
*/
public class Demo3_10 {
	public static void test(Animal animal) {
		animal.eat();
		System.out.println(animal.toString());
	} 
	public static void main(String[] args) throws IOException {
		test(new Cat());
		test(new Dog());
		System.in.read();
	}
} 
abstract class Animal {
	public abstract void eat();
	@Override
	public String toString() {
		return "我是" + this.getClass().getSimpleName();
	}
} 
class Dog extends Animal {
	@Override
	public void eat() {
		System.out.println("啃骨头");
	}
} 
class Cat extends Animal {
	@Override
	public void eat() {
		System.out.println("吃鱼");
	}
}

运行代码:
停在 System.in.read() 方法上,这时运行 jps 获取进程 id
运行 HSDB 工具:
进入 JDK 安装目录,执行 java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
进入图形界面 attach 进程 id
查找某个对象:
打开 Tools -> Find Object By Query
在这里插入图片描述
查看对象内存结构:
点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是MarkWord,后 8 字节就是对象的 Class 指针。
在这里插入图片描述
查看对象 Class 的内存地址:
可以通过 Windows -> Console 进入命令行模式,执行
mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)
结果中第二行 0x000000001b7d4028 即为 Class 的内存地址
请添加图片描述
查看类的 vtable:
Alt+R 进入 Inspector 工具,输入刚才的 Class 内存地址,看到如下界面在这里插入图片描述
多态的方法存放在 vtable表中。
从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址,进行计算0x000000001b7d41e0为vtable的地址。因为 vtable中的长度是6,所以看6行信息。

mem 0x000000001b7d41e0 6
0x000000001b7d41e0: 0x000000001b3d1b10
0x000000001b7d41e8: 0x000000001b3d15e8
0x000000001b7d41f0: 0x000000001b7d35e8
0x000000001b7d41f8: 0x000000001b3d1540
0x000000001b7d4200: 0x000000001b3d1678
0x000000001b7d4208: 0x000000001b7d3fa8

验证方法地址
通过 Tools -> Class Browser 查看每个类的方法定义,比较可知

Dog - public void eat() @0x000000001b7d3fa8
Animal - public java.lang.String toString() @0x000000001b7d35e8;
Object - protected void finalize() @0x000000001b3d1b10;
Object - public boolean equals(java.lang.Object) @0x000000001b3d15e8;
Object - public native int hashCode() @0x000000001b3d1540;
Object - protected native java.lang.Object clone() @0x000000001b3d1678;

对号入座,发现
eat() 方法是 Dog 类自己的
toString() 方法是继承 String 类的
finalize() ,equals(),hashCode(),clone() 都是继承 Object 类的

小结
当执行 invokevirtual 指令时,

  1. 先通过栈帧中的对象引用找到对象
  2. 分析对象头,找到对象的实际 Class
  3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
  4. 查表得到方法的具体地址
  5. 执行方法的字节码

异常处理

try-catch

public class Demo3_11_1 {
	public static void main(String[] args) {
		int i = 0;
		try {
			i = 10;
		} catch (Exception e) {
		i = 20;
		}
	}
}

对应的部分字节码:

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
	stack=1, locals=3, args_size=1
		0: iconst_0
		1: istore_1
		2: bipush 10
		4: istore_1
		5: goto 12
		8: astore_2
		9: bipush 20
		11: istore_1
		12: return
	Exception table:
		from to target type
		  2  5    8    Class java/lang/Exception
	LocalVariableTable:
		Start Length Slot Name Signature
			9      3    2    e   Ljava/lang/Exception;
		    0     13    0  args  Ljava/lang/String;
		    2     11    1    i   I
}

可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围
内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号

8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置

多个 single-catch 块的情况

public class Demo3_11_2 {
	public static void main(String[] args) {
		int i = 0;
		try {
			i = 10;
		} catch (ArithmeticException e) {
			i = 30;
		} catch (NullPointerException e) {
			i = 40;
		} catch (Exception e) {
			i = 50;
		}
	}
}

对应的字节码文件

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
	stack=1, locals=3, args_size=1
		0: iconst_0
		1: istore_1
		2: bipush 10
		4: istore_1
		5: goto 26
		8: astore_2
		9: bipush 30
		11: istore_1
		12: goto 26
		15: astore_2
		16: bipush 40
		18: istore_1
		19: goto 26
		22: astore_2
		23: bipush 50
		25: istore_1
		26: return
	Exception table:
		from to target type
			2 5 8 Class java/lang/ArithmeticException
			2 5 15 Class java/lang/NullPointerException
			2 5 22 Class java/lang/Exception
		LineNumberTable: ...
	LocalVariableTable:
		Start Length Slot Name Signature
		    9      3    2    e  Ljava/lang/ArithmeticException;
		   16      3    2    e  Ljava/lang/NullPointerException;
		   23      3    2    e  Ljava/lang/Exception;
		    0     27    0  args Ljava/lang/String;
		    2     25    1    i    I

因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用

multi-catch 的情况

public class Demo3_11_3 {
	public static void main(String[] args) {
		try {
			Method test = Demo3_11_3.class.getMethod("test");
			test.invoke(null);
		} catch (NoSuchMethodException | IllegalAccessException |
			InvocationTargetException e) {
			e.printStackTrace();
		}
	} 
	public static void test() {
		System.out.println("ok");
	}
}

对应的字节码

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
	stack=3, locals=2, args_size=1
		0: ldc #2
		2: ldc #3
		4: iconst_0
		5: anewarray #4
		8: invokevirtual #5
		11: astore_1
		12: aload_1
		13: aconst_null
		14: iconst_0
		15: anewarray #6
		18: invokevirtual #7
		21: pop
		22: goto 30
		25: astore_1
		26: aload_1
		27: invokevirtual #11 // e.printStackTrace:()V
		30: return
	Exception table:
		from to target type
		   0 22    25  Class java/lang/NoSuchMethodException
		   0 22    25  Class java/lang/IllegalAccessException
		   0 22    25  Class java/lang/reflect/InvocationTargetException
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
   12     10    1 test Ljava/lang/reflect/Method;
   26      4    1    e Ljava/lang/ReflectiveOperationException;
	0     31    0 args Ljava/lang/String;

finally

public class Demo3_11_4 {
	public static void main(String[] args) {
		int i = 0;
		try {
			i = 10;
		} catch (Exception e) {
			i = 20;
		} finally {
			i = 30;
		}
	}
}
public static void main(java.lang.String[]);
	descriptor: ([Ljava/lang/String;)V
	flags: ACC_PUBLIC, ACC_STATIC
	Code:
		stack=1, locals=4, args_size=1
		0: iconst_0
		1: istore_1 		// 0 -> i
		2: bipush 10 		// try --------------------------------------
		4: istore_1 		// 10 -> i 									|
		5: bipush 30 		// finally 									|
		7: istore_1 		// 30 -> i 									|
		8: goto 27 			// return -----------------------------------
		11: astore_2 		// catch Exceptin -> e ----------------------
		12: bipush 20 		// 											|
		14: istore_1 		// 20 -> i 									|
		15: bipush 30 		// finally 									|
		17: istore_1 		// 30 -> i 									|
		18: goto 27 		// return -----------------------------------
		21: astore_3 		// catch any -> slot 3 ----------------------
		22: bipush 30 		// finally 									|
		24: istore_1 		// 30 -> i									|
		25: aload_3 		// <- slot 3 								|
		26: athrow 			// throw ------------------------------------
		27: return
	Exception table:
	from to target type
	   2  5     11 Class java/lang/Exception
       2  5     21 any 			// 剩余的异常类型,比如 Error
	  11 15     21 any 			// 剩余的异常类型,比如 Error

可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程,以确保finally中的代码一定会执行。

finally 面试题
观察下面代码,分析 test()的返回值。

public class Demo3_12_2 {
	public static void main(String[] args) {
		int result = test();
		System.out.println(result);
	} 
	public static int test() {
		try {
			return 10;
		} finally {
			return 20;
		}
	}
}

下面分析字节码文件中对应的指令

Code:
	stack=1, locals=2, args_size=0
		0: bipush 10 			// <- 10 放入栈顶
		2: istore_0 			// 10 -> slot 0 (从栈顶移除了)。伏笔
		3: bipush 20 			// <- 20 放入栈顶
		5: ireturn 				// 返回栈顶 int(20)
		6: astore_1 			// catch any -> slot 1
		7: bipush 20 			// <- 20 放入栈顶
		9: ireturn 				// 返回栈顶 int(20)
	Exception table:
		from to target type
		   0  3      6  any

可以看出,返回值为 20 。
2: istore_0
10 -> slot 0 (从栈顶移除了)。为了在执行完 finally 中的语句,依然能返回正确的值。

在 finally中发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会
吞掉异常。

finally 对返回值影响
分析下面代码 test的返回值。

public class Demo3_12_2 {
	public static void main(String[] args) {
		int result = test();
		System.out.println(result);
	} 
	public static int test() {
		int i = 10;
		try {
			return i;
		} finally {
			i = 20;
		}
	}
}

对应的字节码代码指令

Code:
	stack=1, locals=3, args_size=0
		0: bipush 10 		// <- 10 放入栈顶
		2: istore_0 		// 10 -> i
		3: iload_0 			// <- i(10)
		4: istore_1 		// 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值
		5: bipush 20 		// <- 20 放入栈顶
		7: istore_0 		// 20 -> i
		8: iload_1 			// <- slot 1(10) 载入 slot 1 暂存的值
		9: ireturn 			// 返回栈顶的 int(10)
		10: astore_2
		11: bipush 20
		13: istore_0
		14: aload_2
		15: athrow
	Exception table:
		from to target type
		   3  5     10  any
	4: istore_1 		// 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值
	5: bipush 20 		// <- 20 放入栈顶
	7: istore_0 		// 20 -> i
	8: iload_1 			// <- slot 1(10) 载入 slot 1 暂存的值
	9: ireturn 			// 返回栈顶的 int(10)

可以看出,在执行finally语句块之前,将 返回值保存在 slot 1 中,执行完后,取出 slot 1 ,将值返回

synchronized原理

代码示例

public class Demo3_13 {
	public static void main(String[] args) {
		Object lock = new Object();
			synchronized (lock) {
			System.out.println("ok");
		}
	}
}

对应的字节码文件

Code:
	stack=2, locals=4, args_size=1
		0: new #2 				// new Object
		3: dup
		4: invokespecial #1 	// invokespecial <init>:()V
		7: astore_1 			// lock引用 -> lock
		8: aload_1 				// <- lock (synchronized开始)
		9: dup
		10: astore_2 			// lock引用 -> slot 2
		11: monitorenter 		// monitorenter(lock引用)
		12: getstatic #3 		// <- System.out
		15: ldc #4 				// <- "ok"
		17: invokevirtual #5 	// invokevirtual println:(Ljava/lang/String;)V
		20: aload_2 			// <- slot 2(lock引用)
		21: monitorexit 		// monitorexit(lock引用)
		22: goto 30
		25: astore_3 			// any -> slot 3
		26: aload_2 			// <- slot 2(lock引用)
		27: monitorexit 		// monitorexit(lock引用)
		28: aload_3
		29: athrow
		30: return
	Exception table:
		from to target type
		  12 22     25  any
		  25 28     25  any
	LineNumberTable: ...
	LocalVariableTable:
	Start Length Slot Name Signature
		0     31    0 args  Ljava/lang/String;
		8     23    1 lock  Ljava/lang/Object;

方法级别的 synchronized 不会在字节码指令中有所体现
指令中的第8到20行,就是加锁,解锁的过程。
8: aload_1 将lock的引用加载到操作数栈
9: dup 复制lock到操作数栈顶。
10: astore_2 将lock的引用存放到slot2
11: monitorenter lock 加锁
12-17执行打印方法
20: aload_2 将slot2加载到操作数栈顶
21: monitorexit 解锁
如果上述过程出现异常,则到25行执行解锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值