JVM_03 类文件结构与字节码指令篇

x33g5p2x  于2021-12-06 转载在 Java  
字(10.3k)|赞(0)|评价(0)|浏览(322)

类文件结构与字节码指令

1、类文件结构

一个简单的 HelloWorld.java 程序:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world!");
    }
}

接下来执行:javac -parameters -d . HelloWorld.java 命令编译.java文件为.class文件:

其中-parameters表示将源文件转换为字节码文件的时候保留参数信息

获得二进制字节码文件后怎么读呢?有2种方式:

  • 方式一:JDK自带的反编译工具:javap -verbose XXX.class

  • 方式二:通过notepad++ 查看.class文件,需要装载插件HexEditor,参考文章:notepad++查看二进制.class文件——HexEditor插件(64/32位)安装教程,安装文章中的方式二即可完成,并可以打开.class文件查看二进制字节码:

根据JVM规范类文件的结构如下:

其中u跟数字n,表示占用n个字节

1.1 魔数(u4 magic)

魔数(u4 magic):对应字节码文件的0~3个字节,表示文件的特定类型,不同文件有自己不同的魔数信息,例如java的二进制.class文件的

魔数类型就是如下:

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 091

对于这个cafebabe的由来就不再说了!(每一行的前8位属于编号)

1.2 版本(u2 minor_version、u2 major_version)

版本:对应字节码文件的4~7个字节,表示类的版本 :我们的小版本号在这里不显示,所以前2个字节为00 00

0000000 ca fe ba be00 00 00 3400 23 0a 00 06 00 15 09

这里的十六进制 34H(00 34) 表示十进制的 52,代表的就是JDK8,依次类推:51 就是 JDK 7,53 就是JDK 9。

1.3 常量池
Constant Type 常量类型Value 常量对应的序号(十进制)Value 常量对应的序号(十六进制)
CONSTANT_Class77
CONSTANT_Fieldref99
CONSTANT_Methodref10a
CONSTANT_InterfaceMethodref11b
CONSTANT_String88
CONSTANT_Integer33
CONSTANT_Float44
CONSTANT_Long55
CONSTANT_Double66
CONSTANT_NameAndType12c
CONSTANT_Utf811
CONSTANT_MethodHandle15f
CONSTANT_MethodType1610
CONSTANT_InvokeDynamic1812

案例分析

8~9 字节,表示常量池长度,00 23 (35) 表示常量池有 #1~#34项,注意#0 项不计入,也没有值,注意00表示引用

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

  • #1项 0a (十六进制) 对应的十进制是10,查找常量池表得知为CONSTANT_Methodref,即方法引用(方法信息),00 06 (6) 和 00

15(21) 表示它引用了常量池中 #6#21 项来获得这个方法的【所属类】和【方法名】。

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

  • #2项 09 查找常量池表得知,表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和# 23 项来获得这个成员变量的【所属类】和【成员变量名】

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

  • #3项 08 表示一个字符串常量名称,00 18(24)表示它引用了常量池中#24 项

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

  • #4项 0a 表示一个 Method 信息,00 19(25) 和 00 1a(26) 表示它引用了常量池中 #25 和 #26
    项来获得这个方法的【所属类】和【方法名】

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a07 00 1b 07

  • #5项 07 表示一个 Class 信息,00 1b(27) 表示它引用了常量池中 #27 项

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

  • #6项 07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

  • #7项 01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是【< init > 】

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

  • #8项 01 表示一个 utf8 串,00 03 表示长度,28 29 56 是【() V】其实就是表示无参、无返回值

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

  • #9项 01 表示一个 utf8 串,00 04 表示长度,43 6f 64 65 是【Code】

0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

  • #10项 01 表示一个 utf8 串,00 0f(15) 表示长度,4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65
    是【LineNumberTable】

0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63

1.4 访问标识与继承信息

21 表示该 class 是一个类,公共的类,查下面表中第得到 (0x0001 + 0x0020)ACC_PUBLIC + ACC_SUPER

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 05

表示根据常量池中 #5 找到本类全限定名:

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 06

表示根据常量池中 #6 找到父类全限定名:

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

表示接口的数量,本类为 0:

0 0000660 29 56 00 21 00 05 00 0600 00 00 00 00 02 00 01

1.5 Field 信息

表示成员变量数量,本类为 0

0 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

2、字节码指令

2.1、javap 工具

Java 中提供了 javap 工具来反编译 class 文件

javap -v D:Demo.class
2.2、图解运行流程
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);   
    } 
}

常量池也属于方法区,只不过这里单独提出来了

方法字节码载入方法区

由于:(stack=2,locals=4) 对应操作数栈有 2 个空间(每个空间 4 个字节),局部变量表中有 4 个槽位。

  • stack:最大栈深度,是我们的同时最多能操作多少变量
  • locals:我们一共有多少个变量

栈帧:局部变量表、操作数栈、动态链接、方法出口

执行引擎开始执行字节码

bipush 10:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有

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

istore 1

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

对应代码中的 a = 10,为a变量赋值完成

执行Idc #3将运行时 常量池中的#3压入操作数栈帧

istore2将操作数栈中的元素弹出,放到局部变量表的 2 号位置

iload1 iload2
将局部变量表中 1 号位置和 2 号位置的元素放入操作数栈中。因为只能在操作数栈中执行运算操作

iadd
将操作数栈中的两个元素弹出栈并相加,结果在压入操作数栈中。

istore_3

将我们操作数栈中的计算好的数据,放到局部变量表3的位置

getstatic #4

在运行时常量池中找到 #4 ,发现是一个对象,在堆内存中找到该对象,并将其引用放入操作数栈中

iload #3

将我们局部变量中的3号位置的数据放入我们的操作数栈中

invokevirtual #5

  • 找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法
  • 生成新的栈帧(分配 locals、stack等)
  • 传递参数,执行新栈帧中的字节码

(一个方法一个栈帧)

  • 执行完毕,弹出栈帧
  • 清除 main 操作数栈内容

return

完成 main 方法调用,弹出 main 栈帧,程序结束

2.3、相关练习
/*** * 从字节码的角度分析 a++ 相关题目 */
public class ByteCodeTest {
    public static void main(String[] args) {
        int a = 10 ;
        int b = a++ +  ++a  + a-- ;
        System.out.println(a); //11
        System.out.println(b); //34 : 10 + 12 + 12
    }
}

public class ByteCodeTest {
    public static void main(String[] args) {
      	int i = 0 ;
      	int x = 0 ;				//变量表中的两个变量分别为 0 ,0
      	while(i < 10){
          	x = x++ ;		iload 先将x=0取出到操作数栈中,然后对变量表中x的数据++,然后将操作数栈中的数据itore写回给x
          	i++ ;			因此导致无效add
       }
      System.out.println(x); // 输出0
    }
}
2.4、构造方法
public class Code_12_CinitTest {
	static int i = 10;				//静态成员变量
		
	static {						//静态代码块
	  i = 20;
	}
    
	static {
	  i = 30;
	}

	public static void main(String[] args) {
		System.out.println(i); // 30
	}
}

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

stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #3 // Field i:I
         5: bipush        20
         7: putstatic     #3 // Field i:I
        10: bipush        30
        12: putstatic     #3 // Field i:I
        15: return
public class Code_13_InitTest {

    private String a = "s1";    //成员变量

    {
        b = 20;					//代码块
    }

    private int b = 10;

    {
        a = "s2";
    }

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

    public static void main(String[] args) {
        Code_13_InitTest d = new Code_13_InitTest("s3", 30);  //在init()V之后执行
        System.out.println(d.a);
        System.out.println(d.b);
    }
}

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

注意:如果是静态代码块和静态成员变量,优先级高于{}代码块和成员变量

2.5、方法的调用
public class Code_14_MethodTest {

    public Code_14_MethodTest() {

    }

    private void test1() {			//私有方法 invokespecial

    }

    private final void test2() {	//final方法 invokespecial

    }

    public void test3() {			//普通成员方法 invokevirtual

    }

    public static void test4() {   //静态方法 invokestatic 

    }

    public static void main(String[] args) {
        Code_14_MethodTest obj = new Code_14_MethodTest();
        obj.test1();
        obj.test2();
        obj.test3();
        obj.test4(); //使用对象调用静态方法
        Code_14_MethodTest.test4();
    }
}

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

  • 私有、构造、被 final 修饰的方法,在调用时都使用 invokespecial 指令
  • 普通成员方法在调用时,使用 invokevirtual 指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定
  • 静态方法在调用时使用 invokestatic 指令
Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  //
         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
        15: invokevirtual #6                  // Method test3:()V
        16: aload_1
        17: pop   							
        18: invokestatic  #7 				 //通过对象调用静态方法test4,会浪费2个指令aload1和pop,因此最好不要使用
        20: invokestatic  #8                 // Method test4:()V
        23: return
2.6、多态的原理

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

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

  • 先通过栈帧中对象的引用找到对象
  • 分析对象头,找到对象实际的 Class
  • Class 结构中有 vtable
  • 查询 vtable 找到方法的具体地址
  • 执行方法的字节码
2.7、异常处理
public class Code_15_TryCatchTest {

    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				//为i赋值
        2: bipush        10		
        4: istore_1				//将10写入变量表中的i
        5: goto          12		//结束(不抛出异常到此结束,goto直接到12行)
        8: astore_2				//将我们的异常存入异常表
        9: bipush        20		//20放入操作数栈
       11: istore_1				//20写入变量表中的i
       12: return
     //多出来一个异常表
     Exception table:
        from    to  target type  
            2     5     8   Class java/lang/Exception  当我们的代码在[2,5)中出现异常,直接跳转到8行执行
  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测 2~4 行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 2 号位置(为 e )

多个 single-catch

public class Code_16_MultipleCatchTest {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        }catch (ArithmeticException e) {  //同理muitlcatch(ArithmeticException | Exception 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 位置被共用
  • multicatch同理只是Exception table中是一样的,同样是一个槽保存异常信息

finally

public class Code_17_FinallyTest {
    
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        } finally {		//为什么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 剩余的异常类型流程

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

//我们的结果会是多少呢? 20
public class finallyTest {
    public static void main(String[] args) {
        int result = test() ;
        System.out.println(result);  
    }
    public static int test(){
        try {
            return 10 ;
        }finally {
            return 20 ; 
        }
    }
}

结论:当我们的try语句块中遇到return的时候,会先将return的值保存一下(不会被更改),然后执行finally语句块!

注意:finally块中return导致覆盖了我们的原有的return,所以finally尽量不要用,会吞了我们自己的return

2.8、Synchronized
public class Code_19_SyncTest {

    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                  // class java/lang/Object
         3: dup // 复制一份栈顶,然后压入栈中。用于函数消耗
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1 // 将栈顶的对象地址方法 局部变量表中 1 中
         8: aload_1 // 加载到操作数栈
         9: dup // 复制一份,放到操作数栈,用于加锁时消耗
        10: astore_2 // 将操作数栈顶元素弹出,暂存到局部变量表的 2 号槽位。这时操作数栈中有一份对象的引用
        11: monitorenter // 加锁
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String ok
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2 // 加载对象到栈顶
        21: monitorexit // 释放锁
        22: goto          30
        // 异常情况的解决方案 释放锁!
        25: astore_3
        26: aload_2
        27: monitorexit
        28: aload_3
        29: athrow
        30: return
        // 异常表!
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any
2     5    21   any
       11    15    21   any
可以看到 finally 中的代码被==复制了 3 份==,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程

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


> finally相关面试题

```java
//我们的结果会是多少呢? 20
public class finallyTest {
    public static void main(String[] args) {
        int result = test() ;
        System.out.println(result);  
    }
    public static int test(){
        try {
            return 10 ;
        }finally {
            return 20 ; 
        }
    }
}

结论:当我们的try语句块中遇到return的时候,会先将return的值保存一下(不会被更改),然后执行finally语句块!

注意:finally块中return导致覆盖了我们的原有的return,所以finally尽量不要用,会吞了我们自己的return

2.8、Synchronized
public class Code_19_SyncTest {

    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                  // class java/lang/Object
         3: dup // 复制一份栈顶,然后压入栈中。用于函数消耗
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1 // 将栈顶的对象地址方法 局部变量表中 1 中
         8: aload_1 // 加载到操作数栈
         9: dup // 复制一份,放到操作数栈,用于加锁时消耗
        10: astore_2 // 将操作数栈顶元素弹出,暂存到局部变量表的 2 号槽位。这时操作数栈中有一份对象的引用
        11: monitorenter // 加锁
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String ok
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2 // 加载对象到栈顶
        21: monitorexit // 释放锁
        22: goto          30
        // 异常情况的解决方案 释放锁!
        25: astore_3
        26: aload_2
        27: monitorexit
        28: aload_3
        29: athrow
        30: return
        // 异常表!
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any

结论 : 无论如何,我们通过synchronized加锁的对象最后一定会释放锁!

相关文章