深入理解JVM:类加载和字节码技术(字节码指令详解)
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;
}
}
}
字节码:
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
思考:细心的同学应当注意到,以上比较指令中没有 long,float,double 的比较,那么它们要比较怎么办?
参考 docs.oracle.com/javase/spec…
2.循环控制指令
其实循环控制还是前面介绍的那些指令,例如 while 循环:
public class Demo3_4 {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}
字节码是:
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
再比如 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: retur
注意:比较 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
}
}
4. 构造方法
< cinit>()V 类的构造方法
public class Demo3_8_1 {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
}
编译器会按从上至下的顺序,收集所有 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
< 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
}
}
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后:
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: ...
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();
}
}
字节码:
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
- 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)运行代码
停在 System.in.read() 方法上,这时运行 jps 获取进程 id
2)运行 HSDB 工具,进入 JDK 安装目录,执行:
java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
进入图形界面 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
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
通过 Windows -> Console 进入命令行模式,执行
mem 0x000000001b7d41e0 6
0x000000001b7d41e0: 0x000000001b3d1b10
0x000000001b7d41e8: 0x000000001b3d15e8
0x000000001b7d41f0: 0x000000001b7d35e8
0x000000001b7d41f8: 0x000000001b3d1540
0x000000001b7d4200: 0x000000001b3d1678
0x000000001b7d4208: 0x000000001b7d3fa8
就得到了 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;
对号入座,发现
- eat() 方法是 Dog 类自己的
8)小结
当执行 invokevirtual 指令时:
- 先通过栈帧中的对象引用找到对象。
- 分析对象头,找到对象的实际 Class。
- Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了。
- 查表得到方法的具体地址。
- 执行方法的字节码。
推荐阅读
-
深入理解JVM:类加载和字节码技术(字节码指令详解)
-
Java 类加载器的作用 - 简介:类加载器是 Java™ 中一个非常重要的概念。类加载器负责将 Java 类的字节码加载到 Java 虚拟机中。本文首先详细介绍了 Java 类加载器的基本概念,包括代理模型、加载类的具体过程和线程上下文类加载器等。然后介绍了如何开发自己的类加载器,最后介绍了类加载器在 Web 容器和 OSGi™ 中的应用。 类加载器是 Java 语言的一项创新,也是 Java 语言广受欢迎的重要原因之一。它允许将 Java 类动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 开始出现,最初是为了满足 Java Applets 的需求而开发的,Java Applets 需要从远程位置下载 Java 类文件并在浏览器中执行。现在,类加载器已广泛应用于网络容器和 OSGi。一般来说,Java 应用程序的开发人员不需要直接与类加载器交互;Java 虚拟机的默认行为足以应对大多数情况。但是,如果遇到需要与类加载器交互的情况,而您又不太了解类加载器的机制,就很容易花费大量时间调试异常,如 ClassNotFoundException 和 NoClassDefFoundError。本文将详细介绍 Java 的类加载器,帮助读者深入理解 Java 语言中的这一重要概念。下面先介绍一些基本概念。 类加载器的基本概念 顾名思义,类加载器用于将 Java 类加载到 Java 虚拟机中。一般来说,Java 虚拟机以如下方式使用 Java 类:Java 源程序(.java 文件)经 Java 编译器编译后转换为 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码并将其转换为 java.lang 实例。每个实例都用来表示一个 Java 类。通过该实例的 newInstance 方法创建该类的对象。实际情况可能更加复杂,例如,Java 字节代码可能是由工具动态生成或通过网络下载的。 基本上,所有类加载器都是 java.lang.ClassLoader 类的实例。下面将详细介绍这个 Java 类。 java.lang.ClassLoader 类简介 java.lang.ClassLoader 类的基本职责是根据给定类的名称为其查找或生成相应的字节码,然后根据这些字节码定义一个 Java 类,即 java.lang.Class 类的实例。除此之外,ClassLoader 还负责加载 Java 应用程序所需的资源,如图像文件和配置文件。不过,本文只讨论它加载类的功能。为了履行加载类的职责,ClassLoader 提供了许多方法,其中比较重要的方法如表 1 所示。下文将详细介绍这些方法。 表 1.与加载类相关的 ClassLoader 方法