JVM_08 类加载与字节码技术(字节码指令2)

举报
兴趣使然的草帽路飞 发表于 2021/06/09 00:23:36 2021/06/09
【摘要】 1.条件判断指令 指令助记符含义0x99ifeq判断是否 == 00x9aifne判断是否 != 00x9biflt判断是否 < 00x9cifge判断是否 >= 00x9difgt判断是否 > 00x9eifle判断是否 <= 00x9fif_icmpeq两个int是否 ==0xa0if_icmpne两个int是否 !=0xa1if_icmp...

1.条件判断指令

指令 助记符 含义
0x99 ifeq 判断是否 == 0
0x9a ifne 判断是否 != 0
0x9b iflt 判断是否 < 0
0x9c ifge 判断是否 >= 0
0x9d ifgt 判断是否 > 0
0x9e ifle 判断是否 <= 0
0x9f if_icmpeq 两个int是否 ==
0xa0 if_icmpne 两个int是否 !=
0xa1 if_icmplt 两个int是否 <
0xa2 if_icmpge 两个int是否 >=
0xa3 if_icmpgt 两个int是否 >
0xa4 if_icmple 两个int是否 <=
0xa5 if_acmpeq 两个引用是否 ==
0xa6 if_acmpne 两个引用是否 !=
0xc6 ifnull 判断是否 == null
0xc7 ifnonnull 判断是否 != null

几点说明:

  • 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;
		}
	}
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

字节码:

0: iconst_0
1: istore_1
2: iload_1
3: ifne 		12
6: bipush 		10
8: istore_1
9: goto 		15
12: bipush 		20
14: istore_1
15: return

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

思考:细心的同学应当注意到,以上比较指令中没有 long,float,double 的比较,那么它们要比较怎么办?
参考 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp

2.循环控制指令

其实循环控制还是前面介绍的那些指令,例如 while 循环:

public class Demo3_4 {
	public static void main(String[] args) {
		int a = 0;
		while (a < 10) { a++;
		}
	}
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

字节码是:

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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

再比如 do while 循环:

public class Demo3_5 {
	public static void main(String[] args) {
		int a = 0;
		do { a++;
		} while (a < 10);
	}
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

字节码是:

0: iconst_0 
1: istore_1 
2: iinc 1, 1 
5: iload_1 
6: bipush 10 
8: if_icmplt 2 
11: return

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

最后再看看 for 循环:


public class Demo3_6 {
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
		}
	}
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

字节码是:


0: iconst_0
1: istore_1
2: iload_1
3: bipush 		10
5: if_icmpge 	14
8: iinc 		1, 1
11: goto 		2
14: retur

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注意:比较 while 和 for 的字节码,你发现它们是一模一样的,殊途也能同归

练习 - 判断结果

请从字节码角度分析,下列代码运行的结果:


public class Demo3_6_1 {
	public static void main(String[] args) {
		int i = 0;
		int x = 0;
		while (i < 10) { x = x++; i++;
		}
		System.out.println(x); // 结果是 0
	}
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

4. 构造方法

< cinit>()V 类的构造方法

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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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

0: bipush 		10  // 对应 i = 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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

< cinit>()V 方法会在类加载的初始化阶段被调用

上述代码最终结果 i的值是30!

练习:同学们可以自己调整一下 static 变量和静态代码块的位置,观察字节码的改动

< 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);// s3
		System.out.println(d.b);// 30
	}
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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

public cn.itcast.jvm.t3.bytecode.Demo3_8_2(java.lang.String, int);
	descriptor: (Ljava/lang/String;I)V
	flags: ACC_PUBLIC
	Code:
		stack=2, locals=3, args_size=3 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
		LineNumberTable: ...
		LocalVariableTable: Start Length Slot Name Signature 0 39 0 this Lcn/itcast/jvm/t3/bytecode/Demo3_8_2; 0 39 1 a Ljava/lang/String; 0 39 2 b I
		MethodParameters: ...

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

5.方法调用

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

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();
	}
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

字节码:

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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈。
  • dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “< init>”: ()V (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量。
  • 最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定。
  • 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态。
  • 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】。
  • 比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用。invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了。
  • 还有一个执行 invokespecial 的情况是通过 super 调用父类方法。

6. 多态的原理

/**
 * -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("吃鱼");
	}
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

1)运行代码

停在 System.in.read() 方法上,这时运行 jps 获取进程 id

2)运行 HSDB 工具,进入 JDK 安装目录,执行:

java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

  
 
  • 1

进入图形界面 attach 进程 id,打开 Tools -> Find Object By Query:

输入 select d from cn.itcast.jvm.t3.bytecode.Dog d 点击 Execute 执行

在这里插入图片描述
4)查看对象内存结构

点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是

MarkWord,后 8 字节就是对象的 Class 指针,但目前看不到它的实际地址

在这里插入图片描述
可以通过 Windows -> Console 进入命令行模式,执行

mem 0x00000001299b4978 2

  
 
  • 1

mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)

结果中第二行 0x000000001b7d4028 即为 Class 的内存地址
在这里插入图片描述
6)查看类的 vtable

  • 方法1:Alt+R 进入 Inspector 工具,输入刚才的 Class 内存地址,看到如下界面

在这里插入图片描述

  • 方法2:或者 Tools -> Class Browser 输入 Dog 查找,可以得到相同的结果

在这里插入图片描述
无论通过哪种方法,都可以找到 Dog Class 的 vtable 长度为 6,意思就是 Dog 类有 6 个虚方法(多态相关的,final,static 不会列入)

那么这 6 个方法都是谁呢?从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址,进行计算得到:

0x000000001b7d4028
1b8 +
---------------------
0x000000001b7d41e0

  
 
  • 1
  • 2
  • 3
  • 4

通过 Windows -> Console 进入命令行模式,执行

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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

就得到了 6 个虚方法的入口地址

7)验证方法地址

通过 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;

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

对号入座,发现

  • eat() 方法是 Dog 类自己的

8)小结

当执行 invokevirtual 指令时:

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

文章来源: csp1999.blog.csdn.net,作者:兴趣使然の草帽路飞,版权归原作者所有,如需转载,请联系作者。

原文链接:csp1999.blog.csdn.net/article/details/116353129

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。