JVM学习-字节码指令

目录

  • 1.入门
  • 2 javap 工具
  • 3 图解方法执行流程
    • 3.1.原始 java 代码
    • 3.2.编译后的字节码文件
    • 3.3.常量池载入运行时常量池
    • 3.4.方法字节码载入方法区
    • 3.5.main 线程开始运行,分配栈帧内存
    • 3.6.执行引擎开始执行字节码
  • 4 练习 - 分析 i++
  • 5.条件判断
  • 6.循环控制指令
  • 7 练习 - 判断结果
  • 8 构造方法
  • 9 方法调用
  • 10.多态的原理
  • 11.异常处理
    • 11.1.try-catch
    • 11.2.多个single-catch
    • 11.3.finally
    • 11.4.finally面试题
      • 11.4.1.finally中的return
      • 11.4.2.被吞掉的异常
      • 11.4.3.finally不带return
  • 12.synchronized

1.入门

接着上一节类文件结构,研究一下两组字节码指令,一个是public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令

2a b7 00 01 b1

它实际上对应字节码的指令,java虚拟机内部有解释器,解释器会识别这些平台无关的字节码指令,把它们最终解释为机器码,然后执行。
那么怎么知道机器码对应的字节码指令呢。
请参考
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5
查找0x2a
JVM学习-字节码指令_第1张图片
0x2a aload_0
b7 invokespecial
b1 return

  1. 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数 (把局部变量表中0号槽位的变量加载到操作数栈上)
  2. b7 => invokespecial 预备调用构造方法,哪个方法呢?(准备进行方法的调用)
  3. 00 01 引用常量池中 #1 项,即【 Method java/lang/Object." ": () V 】
  4. b1 表示返回

所以这个是通过this调用了父类的无参构造方法。最后b1是方法执行了要返回。

另一个是 public static void main(java.lang.String[]); 主方法的字节码指令

b2 00 02 12 03 b6 00 04 b1
  1. b2 => getstatic 用来加载静态变量,哪个静态变量呢?
  2. 00 02 引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】
  3. 12 => ldc 加载参数,哪个参数呢?
  4. 03 引用常量池中 #3 项,即 【String hello world】
  5. b6 => invokevirtual 预备调用成员方法,哪个方法呢?
  6. 00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
  7. b1 表示返回

注意,这里字节码是先准备参数,再调用方法。

2 javap 工具

自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件
使用IDEA反编译

F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\cn\yj\jvm\HelloWorld.class
Classfile /F:/IDEA/projects/jvm/out/production/untitled/cn/yj/jvm/HelloWorld.class
  Last modified 2021-2-2; size 553 bytes
  MD5 checksum 6b7033e0eab7845f9c8aa7b8e1f2d44f
  Compiled from "HelloWorld.java"
public class cn.yj.jvm.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // cn/yj/jvm/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcn/yj/jvm/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               hello world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               cn/yj/jvm/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
     
  public cn.yj.jvm.HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 2: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/yj/jvm/HelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 4: 0
        line 5: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

3 图解方法执行流程

3.1.原始 java 代码

package cn.yj.jvm;

/** * 演示 字节码指令 和 操作数栈、常量池的关系 */
public class Demo3_1 {
     
    public static void main(String[] args)
    {
     
        int a = 10;
        int b = Short.MAX_VALUE + 1;
        int c = a + b;
        System.out.println(c);
    }
}

3.2.编译后的字节码文件

F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\cn\yj\jvm\Demo3_1.class
Classfile /F:/IDEA/projects/jvm/out/production/untitled/cn/yj/jvm/Demo3_1.class
  Last modified 2021-2-2; size 603 bytes
  MD5 checksum 9bdbe178a29e07556915f368dbf7def1
  Compiled from "Demo3_1.java"
public class cn.yj.jvm.Demo3_1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#25         // java/lang/Object."":()V
   #2 = Class              #26            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // cn/yj/jvm/Demo3_1
   #7 = Class              #32            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcn/yj/jvm/Demo3_1;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               a
  #20 = Utf8               I
  #21 = Utf8               b
  #22 = Utf8               c
  #23 = Utf8               SourceFile
  #24 = Utf8               Demo3_1.java
  #25 = NameAndType        #8:#9          // "":()V
  #26 = Utf8               java/lang/Short
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(I)V
  #31 = Utf8               cn/yj/jvm/Demo3_1
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (I)V
{
     
  public cn.yj.jvm.Demo3_1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/yj/jvm/Demo3_1;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #3                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 7: 0
        line 8: 3
        line 9: 6
        line 10: 10
        line 11: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            3      15     1     a   I
            6      12     2     b   I
           10       8     3     c   I
}
SourceFile: "Demo3_1.java"

3.3.常量池载入运行时常量池

当我们java代码被执行时,它会由java虚拟机的类加载器把我们main方法所在的类进行类加载的操作,类加载实际上把这些字节的class的数据读取到内存里,常量池的数据被放入运行时常量池,运行时常量池属于方法区的组成部分,只是因为相对比较特殊,其实就是把class文件中的数据存入到运行时常量池的地方。将来找其中一些常量池信息就到运行时常量池中找。
如图只列出了3,4,5几项,第3项是原码中的int b = Short.MAX_VALUE + 1;而int a=10;比较小的数字与方法字节码存储在一起,不存在常量池中。一旦超过了Short整数的最大值的范围,就存到常量池中
常量池也属于方法区,只不过这里单独提出来了
JVM学习-字节码指令_第2张图片

3.4.方法字节码载入方法区

JVM学习-字节码指令_第3张图片

3.5.main 线程开始运行,分配栈帧内存

(stack=2,locals=4) 对应操作数栈有2个空间(每个空间4个字节),局部变量表中有4个槽位
JVM学习-字节码指令_第4张图片

3.6.执行引擎开始执行字节码

bipush 10

  • 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
  • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
  • ldc 将一个 int 压入操作数栈
  • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
  • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池

JVM学习-字节码指令_第5张图片
istore 1

将操作数栈栈顶元素弹出,放入局部变量表的slot 1中0

对应代码中的

a = 10

JVM学习-字节码指令_第6张图片
JVM学习-字节码指令_第7张图片
ldc #3
读取运行时常量池中#3,即32768(超过short最大值范围的数会被放到运行时常量池中),将其加载到操作数栈中

注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的

JVM学习-字节码指令_第8张图片

istore 2
将操作数栈中的元素弹出,放到局部变量表的2号位置
JVM学习-字节码指令_第9张图片
JVM学习-字节码指令_第10张图片
iload_1 iload_2
将局部变量表中1号位置和2号位置的元素放入操作数栈中

因为只能在操作数栈中执行运算操作
JVM学习-字节码指令_第11张图片
JVM学习-字节码指令_第12张图片
iadd

将操作数栈中的两个元素弹出栈并相加,结果在压入操作数栈中
JVM学习-字节码指令_第13张图片
JVM学习-字节码指令_第14张图片
istore 3
将操作数栈中的元素弹出,放入局部变量表的3号位置
JVM学习-字节码指令_第15张图片
JVM学习-字节码指令_第16张图片
getstatic #4

在运行时常量池中找到#4,发现是一个对象

在堆内存中找到该对象,并将其引用放入操作数栈中

JVM学习-字节码指令_第17张图片
JVM学习-字节码指令_第18张图片
iload 3
将局部变量表中3号位置的元素压入操作数栈中

JVM学习-字节码指令_第19张图片
JVM学习-字节码指令_第20张图片
invokevirtual 5

找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法

生成新的栈帧(分配 locals、stack等)

传递参数,执行新栈帧中的字节码

JVM学习-字节码指令_第21张图片
执行完毕,弹出栈帧
清除 main 操作数栈内容

JVM学习-字节码指令_第22张图片
return
完成 main 方法调用,弹出 main 栈帧
程序结束

4 练习 - 分析 i++

目的:从字节码角度分析 a++ 相关题目
源码:

package cn.yj.jvm;

/** * 从字节码角度分析 a++  相关题目 */ public class Demo3_2 {
     

    public static void main(String[] args) {
     
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);
        System.out.println(b);
    }
}

字节码:

F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\cn\yj\jvm\Demo3_2.class
Classfile /F:/IDEA/projects/jvm/out/production/untitled/cn/yj/jvm/Demo3_2.class
  Last modified 2021-2-3; size 578 bytes
  MD5 checksum c5e9d3ebbd57d36a03305a1c3a5d9b4c
  Compiled from "Demo3_2.java"
public class cn.yj.jvm.Demo3_2
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#22         // java/lang/Object."":()V
   #2 = Fieldref           #23.#24        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #25.#26        // java/io/PrintStream.println:(I)V
   #4 = Class              #27            // cn/yj/jvm/Demo3_2
   #5 = Class              #28            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcn/yj/jvm/Demo3_2;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               SourceFile
  #21 = Utf8               Demo3_2.java
  #22 = NameAndType        #6:#7          // "":()V
  #23 = Class              #29            // java/lang/System
  #24 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
  #25 = Class              #32            // java/io/PrintStream
  #26 = NameAndType        #33:#34        // println:(I)V
  #27 = Utf8               cn/yj/jvm/Demo3_2
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/System
  #30 = Utf8               out
  #31 = Utf8               Ljava/io/PrintStream;
  #32 = Utf8               java/io/PrintStream
  #33 = Utf8               println
  #34 = Utf8               (I)V
{
     
  public cn.yj.jvm.Demo3_2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/yj/jvm/Demo3_2;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: bipush        10
         2: istore_1
         3: iload_1
         4: iinc          1, 1
         7: iinc          1, 1
        10: iload_1
        11: iadd
        12: iload_1
        13: iinc          1, -1
        16: iadd
        17: istore_2
        18: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        21: iload_1
        22: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        25: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        28: iload_2
        29: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        32: return
      LineNumberTable:
        line 6: 0
        line 7: 3
        line 8: 18
        line 9: 25
        line 10: 32
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  args   [Ljava/lang/String;
            3      30     1     a   I
           18      15     2     b   I
}
SourceFile: "Demo3_2.java"

分析:
注意 iinc 指令是直接在局部变量 slot 上进行运算
a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc
JVM学习-字节码指令_第23张图片

5.条件判断

指令 助记符 含义
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 用来进行跳转到指定行号的字节码

字节码:

		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

注意,比较小的数用iconst来表示,ifne 12。判断操作数中的栈是不是不等于0,如果不等于0就会跳转到12行,如果不成立,就会执行后面的代码,接着往下走。goto 15是直接跳转到15行。
思考
细心的同学应当注意到,以上比较指令中没有 long,float,double 的比较,那么它们要比较怎 么办?

参考 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp

6.循环控制指令

其实循环控制还是前面介绍的那些指令,例如 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
public class Demo3_5 {
     
	public static void main(String[] args) {
     
		int a = 0;
		do {
     
			a++;
		}while (a < 10);}
	} 
}

后再看看 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 的字节码,你发现它们是一模一样的,殊途也能同归

7 练习 - 判断结果

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

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

x=x++;
x++的过程对应两条字节码指令
iload_x
iinc x,1
初始x(0)
iload是把局部变量表中的0读进操作数栈。读完以后,我们iinc进行自增。自增的结果是局部变量表中的x变为1,它然后又执行了赋值操作,把操作数栈中的0取出来,再覆盖掉局部变量中的x.等第一次循环之后,局部变量表中的x仍然是0,即使再循环多少次,值仍然为0。

8 构造方法

public class Demo3_8_1 {
     
	static int i = 10;
    static {
     
        i = 20;
    }
    static {
     
        i = 30;
    }
    public static void main(String[] args) {
     
        System.out.println(Demo3_8_1.i);
    }
}

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

stack=1, locals=0, args_size=0
         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

最后赋值的是30,所以结果是30

init()V

public class Demo4 {
     
	private String a = "s1";

	{
     
		b = 20;
	}

	private int b = 10;

	{
     
		a = "s2";
	}

	public Demo4(String a, int b) {
     
		this.a = a;
		this.b = b;
	}

	public static void main(String[] args) {
     
		Demo4 d = new Demo4("s3", 30);
		System.out.println(d.a);
		System.out.println(d.b);
	}
}

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

Code:
     stack=2, locals=3, args_size=3
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object."":()V
        4: aload_0
        5: ldc           #2                  // String s1
        7: putfield      #3                  // Field a:Ljava/lang/String;
       10: aload_0
       11: bipush        20
       13: putfield      #4                  // Field b:I
       16: aload_0
       17: bipush        10
       19: putfield      #4                  // Field b:I
       22: aload_0
       23: ldc           #5                  // String s2
       25: putfield      #3                  // Field a:Ljava/lang/String;
       //原始构造方法在最后执行
       28: aload_0
       29: aload_1
       30: putfield      #3                  // Field a:Ljava/lang/String;
       33: aload_0
       34: iload_2
       35: putfield      #4                  // Field b:I
       38: return

在这里插入图片描述
执行顺序:静态代码块->非静态代码块->类的构造方法

9 方法调用

public class Demo5 {
     
	public Demo5() {
     

	}

	private void test1() {
     

	}

	private final void test2() {
     

	}

	public void test3() {
     

	}

	public static void test4() {
     

	}

	public static void main(String[] args) {
     
		Demo5 demo5 = new Demo5();
		demo5.test1();
		demo5.test2();
		demo5.test3();
		Demo5.test4();
	}
}

不同方法在调用时,对应的虚拟机指令有所区别

私有、构造、被final修饰的方法,在调用时都使用invokespecial指令
普通成员方法在调用时,使用invokespecial指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定
静态方法在调用时使用invokestatic指令
invokespecial在调用时无法确定是调用哪个对象的方法,也许是父类的,也许是子类的。invokespecial称之为动态绑定,在运行的时候确定调用哪个对象的方法。invokestatic静态绑定直接就能找到方法的入口地址了。

Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/nyima/JVM/day5/Demo5 
         3: dup
         4: invokespecial #3                  // Method "":()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: invokestatic  #7                  // Method test4:()V
        23: return

new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
dup 是复制操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “init”)V (会消耗掉栈顶一个引用),另一个要 配合 astore_1 赋值给局部变量
终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定
普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
静态方法属于静态绑定。不需要对象来调用

10.多态的原理

/**
 * 演示多态原理,注意加上下面的 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("吃鱼");
    }
}

因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用invokevirtual指令

在执行invokevirtual指令时,经历了以下几个步骤

先通过栈帧中对象的引用找到对象
分析对象头,找到对象实际的Class
Class结构中有vtable
查询vtable找到方法的具体地址
执行方法的字节码

11.异常处理

11.1.try-catch

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

对应字节码指令

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

可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测2~4行)的检测范围
一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
8行的字节码指令 astore_2 是将异常对象引用存入局部变量表的2号位置(为e)

11.2.多个single-catch

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

对应字节码

Code:
     stack=1, locals=3, args_size=1
        0: iconst_0
        1: istore_1
        2: bipush        10
        4: istore_1
        5: goto          19
        8: astore_2
        9: bipush        20
       11: istore_1
       12: goto          19
       15: astore_2
       16: bipush        30
       18: istore_1
       19: return
     Exception table:
        from    to  target type
            2     5     8   Class java/lang/ArithmeticException
            2     5    15   Class java/lang/Exception

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

11.3.finally

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

对应字节码

Code:
     stack=1, locals=4, args_size=1
        0: iconst_0
        1: istore_1
        //try块
        2: bipush        10
        4: istore_1
        //try块执行完后,会执行finally    
        5: bipush        30
        7: istore_1
        8: goto          27
       //catch块     
       11: astore_2 //异常信息放入局部变量表的2号槽位
       12: bipush        20
       14: istore_1
       //catch块执行完后,会执行finally        
       15: bipush        30
       17: istore_1
       18: goto          27
       //出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码   
       21: astore_3
       22: bipush        30
       24: istore_1
       25: aload_3
       26: athrow  //抛出异常
       27: return
     Exception table:
        from    to  target type
            2     5    11   Class java/lang/Exception
            2     5    21   any
           11    15    21   any

可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch剩余的异常类型流程,即catch块捕获不到的异常finally也要执行。

注意:虽然从字节码指令看来,每个块中都有finally块,但是finally块中的代码只会被执行一次

11.4.finally面试题

11.4.1.finally中的return

下面的程序运行结果

public class Demo3 {
     
	public static void main(String[] args) {
     
		int i = Demo3.test();
        //结果为20
		System.out.println(i);
	}

	public static int test() {
     
		int i;
		try {
     
			i = 10;
			return i;
		} finally {
     
			i = 20;
			return i;
		}
	}
}

对应字节码

Code:
     stack=1, locals=3, args_size=0
        0: bipush        10
        2: istore_0
        3: iload_0
        4: istore_1  //暂存返回值
        5: bipush        20
        7: istore_0
        8: iload_0
        9: ireturn	//ireturn会返回操作数栈顶的整型值20
       //如果出现异常,还是会执行finally块中的内容,没有抛出异常
       10: astore_2
       11: bipush        20
       13: istore_0
       14: iload_0
       15: ireturn	//这里没有athrow了,也就是如果在finally块中如果有返回操作的话,且try块中出现异常,会吞掉异常!
     Exception table:
        from    to  target type
            0     5    10   any

由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以finally的为准
至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常,这样就不知道发生了异常
所以不要在finally中进行返回操作

11.4.2.被吞掉的异常

public class Demo3 {
     
   public static void main(String[] args) {
     
      int i = Demo3.test();
      //最终结果为20
      System.out.println(i);
   }

   public static int test() {
     
      int i;
      try {
     
         i = 10;
         //这里应该会抛出异常
         i = i/0;
         return i;
      } finally {
     
         i = 20;
         return i;
      }
   }
}

会发现打印结果为20,并未抛出异常

11.4.3.finally不带return

public class Demo4 {
     
	public static void main(String[] args) {
     
		int i = Demo4.test();
		System.out.println(i);
	}

	public static int test() {
     
		int i = 10;
		try {
     
			return i;
		} finally {
     
			i = 20;
		}
	}
}

结果是10
对应字节码

Code:
     stack=1, locals=3, args_size=0
        0: bipush        10
        2: istore_0 //赋值给i 10
        3: iload_0	//加载到操作数栈顶
        4: istore_1 //加载到局部变量表的1号位置
        5: bipush        20
        7: istore_0 //赋值给i 20
        8: iload_1 //加载局部变量表1号位置的数10到操作数栈
        9: ireturn //返回操作数栈顶元素 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

如果在try语句return了,finally中虽然对i做了变化,但是不会影响到返回结果的,因为它在return之前先做了一个暂存,然后再执行了finally中的代码,然后再把暂存的值取回来,最后返回的是return时暂存的值。

12.synchronized

public class Demo5 {
     
	public static void main(String[] args) {
     
		int i = 10;
		Lock lock = new Lock();
		synchronized (lock) {
     
			System.out.println(i);
		}
	}
}

对应字节码

Code:
     stack=2, locals=5, args_size=1
        0: bipush        10
        2: istore_1
        3: new           #2                  // class com/nyima/JVM/day06/Lock
        6: dup //复制一份,放到操作数栈顶,用于构造函数消耗
        7: invokespecial #3                  // Method com/nyima/JVM/day06/Lock."":()V
       10: astore_2 //剩下的一份放到局部变量表的2号位置
       11: aload_2 //加载到操作数栈
       12: dup //复制一份,放到操作数栈,用于加锁时消耗
       13: astore_3 //将操作数栈顶元素弹出,暂存到局部变量表的三号槽位。这时操作数栈中有一份对象的引用
       14: monitorenter //加锁
       //锁住后代码块中的操作    
       15: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       18: iload_1
       19: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
       //加载局部变量表中三号槽位对象的引用,用于解锁    
       22: aload_3    
       23: monitorexit //解锁
       24: goto          34
       //异常操作    
       27: astore        4
       29: aload_3
       30: monitorexit //解锁
       31: aload         4
       33: athrow
       34: return
     //可以看出,无论何时出现异常,都会跳转到27行,将异常放入局部变量中,并进行解锁操作,然后加载异常并抛出异常。      
     Exception table:
        from    to  target type
           15    24    27   any
           27    31    27   any

不管是方法正常执行,还是中间出现了异常,还总是会进入到25行加载刚才暂存的lock对象的引用来配合monitorexit进行一个解锁操作

注意
方法级别的 synchronized 不会在字节码指令中有所体现

你可能感兴趣的:(JVM,Java,jvm,java)