java字节码

292 阅读58分钟

字节码概述

计算机只认识0和1。这意味着任何语言编写的程序最终都需要经过编译器编译成机器码才能被计算机执行。所以,我们所编写的程序在不同的平台上运行前都要经过重新编译才能被执行。 而Java刚诞生的时候曾经提过一个非常著名的宣传口号: "一次编写,到处运行"

Write Once,Run Anywhere.

为了实现该目的,Sun公司以及其他虚拟机提供商发布了许多可以运行在不同平台上的JVM虚拟机,而这些虚拟机都拥有一个共同的功能,那就是可以载入和执行同一种与平台无关的字节码(ByteCode)。 于是,我们的源代码不再必须根据不同平台翻译成0和1,而是间接翻译成字节码,储存字节码的文件再交由运行于不同平台上的JVM虚拟机去读取执行,从而实现一次编写,到处运行的目的。 如今,JVM也不再只支持Java,由此衍生出了许多基于JVM的编程语言,如Groovy, Scala, Koltin等等。

Java虚拟机

字节码组成

public class Solution {

	public String name;
	public int age;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
}

编译后得到:

00000000: cafe babe 0000 0037 001d 0a00 0500 1809  .......7........
00000010: 0004 0019 0900 0400 1a07 001b 0700 1c01  ................
00000020: 0004 6e61 6d65 0100 124c 6a61 7661 2f6c  ..name...Ljava/l
00000030: 616e 672f 5374 7269 6e67 3b01 0003 6167  ang/String;...ag
00000040: 6501 0001 4901 0006 3c69 6e69 743e 0100  e...I...<init>..
00000050: 0328 2956 0100 0443 6f64 6501 000f 4c69  .()V...Code...Li
00000060: 6e65 4e75 6d62 6572 5461 626c 6501 0007  neNumberTable...
00000070: 6765 744e 616d 6501 0014 2829 4c6a 6176  getName...()Ljav
00000080: 612f 6c61 6e67 2f53 7472 696e 673b 0100  a/lang/String;..
00000090: 0773 6574 4e61 6d65 0100 1528 4c6a 6176  .setName...(Ljav
000000a0: 612f 6c61 6e67 2f53 7472 696e 673b 2956  a/lang/String;)V
000000b0: 0100 0667 6574 4167 6501 0003 2829 4901  ...getAge...()I.
000000c0: 0006 7365 7441 6765 0100 0428 4929 5601  ..setAge...(I)V.
000000d0: 000a 536f 7572 6365 4669 6c65 0100 0d53  ..SourceFile...S
000000e0: 6f6c 7574 696f 6e2e 6a61 7661 0c00 0a00  olution.java....
000000f0: 0b0c 0006 0007 0c00 0800 0901 000c 636f  ..............co
00000100: 6d2f 536f 6c75 7469 6f6e 0100 106a 6176  m/Solution...jav
00000110: 612f 6c61 6e67 2f4f 626a 6563 7400 2100  a/lang/Object.!.
00000120: 0400 0500 0000 0200 0100 0600 0700 0000  ................
00000130: 0200 0800 0900 0000 0500 0100 0a00 0b00  ................
00000140: 0100 0c00 0000 1d00 0100 0100 0000 052a  ...............*
00000150: b700 01b1 0000 0001 000d 0000 0006 0001  ................
00000160: 0000 0006 0001 000e 000f 0001 000c 0000  ................
00000170: 001d 0001 0001 0000 0005 2ab4 0002 b000  ..........*.....
00000180: 0000 0100 0d00 0000 0600 0100 0000 0b00  ................
00000190: 0100 1000 1100 0100 0c00 0000 2200 0200  ............"...
000001a0: 0200 0000 062a 2bb5 0002 b100 0000 0100  .....*+.........
000001b0: 0d00 0000 0a00 0200 0000 0f00 0500 1000  ................
000001c0: 0100 1200 1300 0100 0c00 0000 1d00 0100  ................
000001d0: 0100 0000 052a b400 03ac 0000 0001 000d  .....*..........
000001e0: 0000 0006 0001 0000 0013 0001 0014 0015  ................
000001f0: 0001 000c 0000 0022 0002 0002 0000 0006  ......."........
00000200: 2a1b b500 03b1 0000 0001 000d 0000 000a  *...............
00000210: 0002 0000 0017 0005 0018 0001 0016 0000  ................
00000220: 0002 0017 0a                           ssd..

文件开头的4个字节称之为 魔数,唯有以"cafe babe"开头的class文件方可被虚拟机所接受,这4个字节就是字节码文件的身份识别。 目光右移,0000是编译器jdk版本的次版本号0,0037转化为十进制是55,是主版本号,java的版本号从45开始,除1.0和1.1都是使用45.x外,以后每升一个大版本,版本号加一。也就是说,编译生成该class文件的jdk版本为11.0。 通过java -version命令稍加验证, 可得结果。

pisceszhang@zhangyunfeideMacBook-Pro com % openjdk version "11.0.12" 2021-07-20
OpenJDK Runtime Environment Homebrew (build 11.0.12+0)
OpenJDK 64-Bit Server VM Homebrew (build 11.0.12+0, mixed mode)

直接使用Java工具dump这个class文件,dump命令如下,同理其实自己也可以查表,手动反编译:

javap -v -p Solution.class


//类相关信息
———————————————————————————————————————————————————————————————————————————————————public class com.Solution
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #4                          // com/Solution
  super_class: #5                         // java/lang/Object
  interfaces: 0, fields: 2, methods: 5, attributes: 1
//常量池
———————————————————————————————————————————————————————————————————————————————————Constant pool:
   #1 = Methodref          #5.#24         // java/lang/Object."<init>":()V
   #2 = Fieldref           #4.#25         // com/Solution.name:Ljava/lang/String;
   #3 = Fieldref           #4.#26         // com/Solution.age:I
   #4 = Class              #27            // com/Solution
   #5 = Class              #28            // java/lang/Object
   #6 = Utf8               name
   #7 = Utf8               Ljava/lang/String;
   #8 = Utf8               age
   #9 = Utf8               I
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               getName
  #15 = Utf8               ()Ljava/lang/String;
  #16 = Utf8               setName
  #17 = Utf8               (Ljava/lang/String;)V
  #18 = Utf8               getAge
  #19 = Utf8               ()I
  #20 = Utf8               setAge
  #21 = Utf8               (I)V
  #22 = Utf8               SourceFile
  #23 = Utf8               Solution.java
  #24 = NameAndType        #10:#11        // "<init>":()V
  #25 = NameAndType        #6:#7          // name:Ljava/lang/String;
  #26 = NameAndType        #8:#9          // age:I
  #27 = Utf8               com/Solution
  #28 = Utf8               java/lang/Object
{
//字段表集合
  public java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC

  private int age;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE
//方法表合集和其属性表
  public com.Solution();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
//方法表合集和其属性表  
public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 11: 0

  public void setName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field name:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 15: 0
        line 16: 5

  public int getAge();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #3                  // Field age:I
         4: ireturn
      LineNumberTable:
        line 19: 0

  public void setAge(int);
    descriptor: (I)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #3                  // Field age:I
         5: return
      LineNumberTable:
        line 23: 0
        line 24: 5
}
SourceFile: "Solution.java"

以上字节码可以分为:包含声明的类信息,常量池,字段表(field_info,字段信息,成员变量),方法表(method_info),属性表(attribute_info)。

类相关信息

开头的7行信息包括:Class文件当前所在位置,最后修改时间,文件大小,MD5值,编译自哪个文件,类的全限定名,jdk次版本号,主版本号。 然后紧接着的是该类的访问标志:ACC_PUBLIC, ACC_SUPER,访问标志的含义如下:

常量池

Constant pool意为常量池。 常量池可以理解成Class文件中的资源仓库。主要存放的是两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量类似于java中的常量概念,如文本字符串,final常量等,而符号引用则属于编译原理方面的概念,包括以下三种:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符号(Descriptor)
  • 方法的名称和描述符

不同于C/C++, JVM是在加载Class文件的时候才进行的动态链接,也就是说这些字段和方法符号引用只有在运行期转换后才能获得真正的内存入口地址。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析并翻译到具体的内存地址中。

总而言之,常量池中存放的就是一些常用的字段,其他部分对其进行引用,节约资源。

字段表

  字段表用于描述接口或者类中声明的变量,**包括类级别和对象级别的变量**,但是不包括局部变量。

private int m;
  descriptor: I
  flags: ACC_PRIVATE

方法表

 方法表和字段表用法基本一致

  public com.Solution();
    descriptor: ()V    参数和返回值:(参数)返回值
    flags: (0x0001) ACC_PUBLIC

方法执行的过程在“Code”中,这部分放在属性表中,将在下部分详细陈诉。

属性表

attribute_info不是一个单独的区域,他是为了补充说明增加的部分,分散在方法表,字段表,类文件各个地方。

以下对Code属性进行说明:

 public void setName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field name:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 15: 0
        line 16: 5
  • stack

最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度,此处为1

  • locals:

局部变量所需的存储空间,单位为Slot, Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小。方法参数(包括实例方法中的隐藏参数this),显示异常处理器的参数(try catch中的catch块所定义的异常),方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是,locals的大小并不一定等于所有局部变量所占的Slot之和,因为局部变量中的Slot是可以重用的。

  • args_size:

方法参数的个数,这里是2,因为每个实例方法都会有一个隐藏参数this。方法通过这种方式访问对象变量,即在编译时将this变为普通参数。这个处理只对实例方法有效,假如static方法就不会进行上诉操作,因为static属于类。

  • attribute_info

方法体内容,0,1,4为字节码"行号",该段代码的意思是将第一个引用类型本地变量推送至栈顶,然后执行该类型的实例方法,也就是常量池存放的第一个变量,也就是注释里的"java/lang/Object."":()V", 然后执行返回语句,结束方法。

  • LineNumberTable

该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。可以使用 -g:none 或-g:lines选项来取消或要求生成这项信息,如果选择不生成LineNumberTable,当程序运行异常时将无法获取到发生异常的源码行号,也无法按照源码的行数来调试程序。

  • LocalVariableTable

该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。可以使用 -g:none 或 -g:vars来取消或生成这项信息,如果没有生成这项信息,那么当别人引用这个方法时,将无法获取到参数名称,取而代之的是arg0, arg1这样的占位符。 start 表示该局部变量在哪一行开始可见,length表示可见行数,Slot代表所在帧栈位置,Name是变量名称,然后是类型签名。

同理可以分析Main类中的另一个方法"inc()": 方法体内的内容是:将this入栈,获取字段#2并置于栈顶, 将int类型的1入栈,将栈内顶部的两个数值相加,返回一个int类型的值。

根据以上说明,对上诉代码的执行过程进行说明。

例1:

Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field name:Ljava/lang/String;
         5: return

1.aload_0:将0号变量加载到栈中,每个方法的0号变量即this。

2.aload_1:将1号变量加载到栈中。

3.对变量赋值。

4.返回为空。

下面分析异常的字节码

例2:单catch

try-catch

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

例3:多catch

public class Code_16_MultipleCatchTest {

    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

例4:finally

public class Code_17_FinallyTest {
    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 剩余的异常类型流程
注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次

例5:finally-return

public class Code_18_FinallyReturnTest {

    public static void main(String[] args) {
        int i = Code_18_FinallyReturnTest.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中进行返回操作 

参考文献:

blog.csdn.net/weixin\_502…

juejin.cn/post/684490…

juejin.cn/post/684490…