指令
条件判断指令
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 指令时,
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际 Class
- Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查表得到方法的具体地址
- 执行方法的字节码
异常处理
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行执行解锁。