class文件和字节码解析

通过《编译原理》的相关学习,我们知道我们编写的Java代码最终会被翻译成class文件。Class文件格式是JVM自己定义的用于表示Java类的二进制字节流规范,与操作系统本身无关,该文件格式正是Java代码一次编译,跨平台运行的关键。

class文件


其中u 表示n个无符号字节,如u4 magic 表示magic的取值用4个无符号字节表示,cp_info描述常量池的结构,field_info描述字段的数据结构,method_info描述方法的数据结构,attribute_info描述属性的数据结构。ClassFile结构各项的含义如下:

  1. magic: 魔数,用于标识当前Class文件的文件格式,JVM可据此判断该文件是否可以被解析,目前固定为0xCAFEBABE
  2. minor_version, major_version:minor_version是副版本号,major_version是主版本号,这两个版本是生成Class文件时根据编译的JDK版本来确定的,用标识编译时的JDK版本,常见的一个异常Unsupported
    major.minor version 52.0就是因为运行时的JDK版本低于编译时的JDK版本,52是Java8的主版本号。
  3. constant_pool_count:常量池计数器,等于常量池中的成员数加1
  4. constant_pool:常量池,是一种表结构,包含class文件结构和子结构中引用的所有字符串常量,类或者接口名,字段名和其他常量,其有效索引范围是1- (constant_pool_count-1)。其中类和接口名采用全限定形式,即在整个JVM中的绝对名称,如java.lang.Object,方法名,字段名、局部变量名和形参名都采用非限定名,即在源代码文件中使用相对名称,如属性名name。
  5. access_flags:用于表示某个类或者接口的访问权限和属性
  6. this_class:类索引,该值必须是对常量池中某个常量的一个有效索引值,该索引处的成员必须是一个CONSTANT_Class_info类型的结构体,表示这个class文件所定义的类和接口
  7. super_class:父类索引,同this_class,该值必须是对常量池中CONSTANT_Class_info类型常量的一个有效索引值,如果该值为0,则只能表示java.lang.Object类,因为该类是唯一一个没有父类的类。
  8. interfaces_count:接口计数器,表示当前类或者接口的直接超接口的数量
  9. interfaces:接口表,是一个表结构,每个成员同this_class,必须是对常量池中CONSTANT_Class_info类型常量的一个有效索引值,其有效索引范围为0~interfaces_count,接口表中成员的顺序与源代码中给定的接口顺序是一致的,interfaces[0]表示源代码中最左边的接口。
  10. fields_count:字段计数器,当前class文件所有字段的数量
  11. fields:字段表,是一个表结构,表中每个成员必须是filed_info数据结构,用于表示当前类或者接口的某个字段的完整描述,不包含从父类或者父接口继承的字段
  12. methods_count:方法计数器,表示当前类方法表的成员个数
  13. methods:方法表,是一个表结构,表中每个成员必须是method_info数据结构,用于表示当前类或者接口的某个方法的完整描述,包含当前类或者接口定义的所有方法,如实例方法、类方法、实例初始化方法等,不包含从父类或者父接口继承的方法
  14. attributes_count:属性计数器,表示当前class文件attributes属性表的成员个数
  15. attributes:属性表,是一个表结构,表中每个成员必须是attribute_info数据结构,这里的属性是对class文件本身,方法或者字段的补充描述,如SourceFile属性用于表示class文件的源代码文件名。
// 为是一个简单的例子
public class ClassFileTest {
    private int a = 5;
    public int getA(){
        return this.a;
    }
    public static void main(String[] args) {
        new ClassFileTest().getA();
    }
}
// javap -v
public class ClassFileTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#25         // ClassFileTest.a:I
   #3 = Class              #26            // ClassFileTest
   #4 = Methodref          #3.#24         // ClassFileTest."<init>":()V
   #5 = Methodref          #3.#27         // ClassFileTest.getA:()I
   #6 = Class              #28            // java/lang/Object
   #7 = Utf8               a
   #8 = Utf8               I
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               LClassFileTest;
  #16 = Utf8               getA
  #17 = Utf8               ()I
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               args
  #21 = Utf8               [Ljava/lang/String;
  #22 = Utf8               SourceFile
  #23 = Utf8               ClassFileTest.java
  #24 = NameAndType        #9:#10         // "<init>":()V
  #25 = NameAndType        #7:#8          // a:I
  #26 = Utf8               ClassFileTest
  #27 = NameAndType        #16:#17        // getA:()I
  #28 = Utf8               java/lang/Object
{
  public ClassFileTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_5
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 1: 0
        line 3: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LClassFileTest;

  public int getA();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: ireturn
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LClassFileTest;

  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: new           #3                  // class ClassFileTest
         3: dup
         4: invokespecial #4                  // Method "<init>":()V
         7: invokevirtual #5                  // Method getA:()I
        10: pop
        11: return
      LineNumberTable:
        line 10: 0
        line 11: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  args   [Ljava/lang/String;
}

例如#2对应类型是Fieldref,表示为ClassFileTest.a:I.
例如#5对应类型是Methodref,表示为ClassFileTest.getA:()I.
什么意思?

描述符

描述符有两种,字段描述符和方法描述符,本质就是一个基于特定规则的字符串,其中字段描述符用来表示类,实例和局部变量的类型,具体如下:在这里插入图片描述
ClassFileTest.a:I 表示 ClassFileTest类中int成员变量a.
ClassFileTest.getA:()I.表示 ClassFileTest类中返回值是int的getA方法。

常量池

Java虚拟机指令不依赖类,接口,类实例或数组的运行时内存布局,而是依赖依赖常量池表中的符号信息,常量池表中所有项都有如下通用格式:

cp_info{
   u1 tag;//类型标记,用于确定后面的info的格式,tag是一个字节
   u1 info[];//两个或者多个字节,取决于tag的值
}

在这里插入图片描述
CONSTANT_Utf8_info:用于表示一个Utf8编码的字符串

CONSTANT_Utf8_info{
   u1 tag;//tag取值1
   u2 length;//后面的byte数组的长度
   u1 bytes[length];//字符串对应的byte数组数据
}

CONSTANT_Class_info:用于表示一个Java类或者接口名

CONSTANT_Class_info{
   u1 tag;//tag取值7
   u2 name_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
}

CONSTANT_Fieldref_info:用于描述一个字段

CONSTANT_Fieldref_info{
   u1 tag;//tag取值9
   u2 class_index;//常量池中的有效索引,该索引处的成员必须是一个CONSTANT_Class_info结构,表示该字段所属的类
   u2 name_and_type_index;//常量池中的有效索引,该索引处的成员必须是一个CONSTANT_NameAndType_info结构,该结构用于表示一个字段或者方法描述符。
}

CONSTANT_MethodType_info:用于记录方法的类型信息,即方法描述符

CONSTANT_MethodType_info{
   u1 tag;//tag取值21
   u2 name_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
   u2 descriptor_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
}

更多类型请参考《Java虚拟机规范8版》

字段

feild_info{
   u2 access_flags;//字段标识
   u2 name_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
   u2 descriptor_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
   u2 attributes_count;//当前字段附加属性数值
   attribute_info attributes[attributes_count];//属性表中的每个成员
}

在这里插入图片描述

方法

method_info{
   u2 access_flags;//方法标识
   u2 name_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
   u2 descriptor_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
   u2 attributes_count;//当前字段附加属性数值
   attribute_info attributes[attributes_count];//属性表中的每个成员
}

在这里插入图片描述

属性

ClassFile、filed_info、method_info结构和Code属性都有属性表,所有的属性都通过attribute_info结构表示,其通用格式如下:

attribute_info{
   u2 attribute_name_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
   u4 attribute_length;//表示后面的info信息的字节长度
   u1 info[attribute_length];//具体数据
}

Java8预定义了23种属性(《Java虚拟机规范8版》中有介绍),例

Code:位于method_info的属性表中,表示该方法的虚拟机指令及辅助信息,method_info中有且仅有一个Code属性,其结构如下:

attribute_info{
   u2 attribute_name_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
   u4 attribute_length;//表示后面的info信息的字节长度
   u2 max_stack;//当前方法操作数栈的最大深度
   u2 max_locals;//此方法引用局部变量表中的局部变量的个数,包含传递方法入参的局部变量
   u4 code_length;//后面的code数组的字节长度
   u1 code[code_length];//当前方法的虚拟机指令的数据
   u2 exception_table_length;//后面的exception_table数组的长度;
   {
      u2 start_pc;
      u2 end_pc;//try/catch的代码范围,具体来说是起止代码对应的虚拟机指令在code数组中的索引
      u2 handler_pc;//异常处理逻辑的代码的虚拟机指令在code数组中的索引
      u2 catch_type;//常量池中一个类型为CONSTANT_Class_info的有效索引,表示捕获的异常类型。
   }exception_table[exception_table_length];//此方法的捕获的各异常的异常处理逻辑
}

用户在编译源代码文件时可以添加新的属性,只要JVM实现能够正确识别该属性即可,注意用户自定义的属性不能使用已有预定义属性的属性名

虚拟机指令集

C/C++的方法会被编译成特定于CPU架构的汇编指令,然后交由CPU逐一执行,因为汇编指令与CPU架构是强绑定的,所以C/C++程序在执行前需要在不同CPU架构的机器上编译一遍。Java为了实现一处编译,跨平台运行的目标,在汇编指令之上引入了一个独立于平台的中间层,虚拟机指令,由Java虚拟机规范提供指令标准定义,由Java虚拟机厂商提供指令实现,不同平台的Java虚拟机都遵循相同的指令集规范,从而实现跨平台运行目标。一个方法对应的一组虚拟机指令称为这个方法的字节码(byte codes)。

具体虚拟机指令集请查询《Java虚拟机规范8版》,例:

  public int getA();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: ireturn
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LClassFileTest;
操作码 助记符 指令含义 例子操作
42 aload_0 将第一个引用类型本地变量推送至栈顶 this对象放入栈顶
180 getfield 获取指定类的实例字段,并将值压入栈顶 获取a的值
172 ireturn 从当前方法返回int 返回a的值

主要参考

《hotspot实战》
《Java虚拟机规范8版》
Hotspot class文件和字节码解析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章