Java Class文件格式的理解

440 阅读10分钟

写博客真的是一件花费耐心的事情。
最近在学习Android ART虚拟机,Class文件是Java虚拟机的可执行文件是必不可少的前置知识点。
如果你想对Class文件有一定程度的了解,希望它会对你有所帮助。

Class文件格式总览

alt
magic: 魔数,是class文件的规范,u4代表4个字节的长度,取值必须是0xCAFEBABE

minor_version & major_version: 两个字节长度,分别代表class文件的小版本信息和大版本信息

constant_pool_count: 表示常量池数组中元素的个数,constant_pool是一个存储cp_info信息的数组,每一个class文件都包含一个常量池。常量池在代码中对应一个数组,其元素类型就是cp_info.

access_flags: 标明该类的访问权限,比如public、private等(容易联想到反射的setAccessible())

this_class & super_class: 存储的是指向常量池数组元素的索引。通过这两个索引和常量池对应元素的内容,可以知道本类父类的类名

interfaces_count & interfaces: 同样是常量池数组里的索引号,表示该类实现了多少个接口以及接口类的类名

fields_count & fields: 表示成员变量的数量和信息。其中成员变量的信息由field_info结构体表示

methods_count & methods: 表示成员函数的数量和信息,其中成员函数的信息由method_info结构体表示

attributes_count & attributes: 该类包含的属性信息。属性信息由attributes_info结构体表示。

u4:表示长度为4个字节 ,同理u2代表两个字节

\color{red}{这些前置只能帮我们解析一个Class文件,想要真正看懂Class文件以下是必须要了解的} \color{red}{1、常量池包括常量项的类型和几种主要常量项和他们之间的关系}

\color{red}{2、用于描述成员变量的field info和成员函数的method info}

\color{red}{3、访问标志}

\color{red}{4、属性信息}

常量池及相关内容

常量池的类型和关系

虚拟机中的常量池对应的数据结构类型为cp_info的数组。每一个cp_info对象存储了一个常量项。cp_info的伪代码如下:

cp_info {
    u1  tag;        //每一个cp_info的第一个字节表明该常量项的类型
    u1  info[];     //第二个字节表示常量项的具体内容
}

常量项的类型如下图所示(tag类型):

本来想找个网图偷个懒,但是发现网图都有描述不清的歧义,尤其是CONSTANT_String

注意CONSTANT_String 和CONSTANT_Utf8的区别


常见常量项的内容:

Utf8、Class等对应的数据结构:

CONSTANT_Utf8_info{         CONSTANT_Class_info{        CONSTANT_Fiedldref_info{
    u1  tag;                    u1  tag;                    u1  tag;
    u2  length;                 u2  name_index;             u2  class_index;
    u1  bytes[length];      }                               u2  name_and_type_index;
}                                                        }

CONSTANT_String_info{       CONSTANT_MethodType_info{   CONSTANT_Methodref_info{
    u1  tag;                    u1  tag;                    u1  tag;
    u2  string_index;           u2  descriptor_index;       u2  class_index;
}                           }                               u2  name_and_type_index;
                                                        }

Long、Integer等对应的数据结构:

CONSTANT_Long_info{             CONSTANT_Integer_info{
    u1  tag;                        u1  tag;
    u4  high_bytes;                 u4  bytes;
    u4  low_bytes;              }
}

CONSTANT_Double_info{           CONSTANT_Float_info{
    u1  tag;                        u1  tag;
    u4  high_bytes;                 u4  bytes;
    u4  low_bytes;              }
}

我们以CONSTANT_Utf8_info为例子:

其中length表示bytes数组的长度,而bytes成员则真正存储字符串的内容。Class等对应的数据结构中的index实际上都是指向常量池中CONSTANAT_Utf8_info元素的索引。

和String_info一样,Class_info里面的name_index、MethodType_info里面的descriptor_index、NameAndType_info里面的name_index和descriptor_index都代表一个指向类型为Utf8_info元素的索引。

为什么在Double_info、Integer_info等结构体中直接存储数据,而String_info、Class_info等需要采用这种间接元素索引的方式呢?

原因很简单,为了节省Class文件的空间,我们举个例子。

public class Sample{                                            CONSTANT_Fieldref_info{
    public String name;     这个Sample.java编译成Sample.class后     u1  tag;
    public String hobby;    将包含两个CONSTANT_Fieldref_info        u2  class_index;
}                                                                 u2   name_and_type_index
                                                                }
(1)当class_index不再是索引的时候,要存储类的信息Sample

(2)name_and_type_index也不再是索引,而是name和hobby的具体内容:"name"和"Ljava/lang/String;"
 "hobby"和"Ljava/lang/String;"   name和hobby是变量名字,Ljava/lang/String;是变量数据类型的字符串表示
 
 显然上面就出现了冗余信息,Ljava/lang/String; 和 Sample就多存了一份。

信息描述规则

根据Java虚拟机规范,如何用字符串换来描述成员变量、成员函数是有讲究的,这些规则主要集中在数据类型,成员变量和成员函数的描述:
(1)数据类型(比如原始数据类型,引用数据类型)的描述规则
(2)成员变量的描述规则,规范里面称作Field Descriptor
(3)成员函数的描述规则,规范里面称作Method Descriptor

数据类型描述规则

(1)原始数据类型对应的字符串描述为B C D F I J S Z,他们分别对应的Java类型为byte、char、double、float、int、long、short、boolean.

(2) 引用数据类型的格式为LClassName。此处的ClassName为对应类的全路径名,比如刚才提到的Ljava/lang/String。全路径名的"."号由"/"替代,并且最后必须带分号。

(3)数组也是一种引用类型,数组用"[其他类型的描述名",比如int数组为"[I",字符串数组就是""[Ljava/lang/String;"

成员变量的描述规则

成员变量 Field Descriptor的描述类型就是前面说的数据类型,举个例子:

FieldDescriptor:
FieldType               #FieldDescriptor描述规则只包含FieldType的一种信息
FieldType:              #FieldType描述的信息由这三个组成 BaseType、ObjectType、ArrayType
BaseType:               #原始数据类型 B C D F I J S Z
ObjectType:             #引用类型   LClassName
ArrayType:              #数组类型,由[加Componentpe构成
[Componentpe:           #是一个新东西(暂时知道这个东西就好了)
ComponentType:          #定义ComponentType,他由上面定义的FieldType构成
FieldType

成员函数的描述规则

和成员变量的描述略有不同,一个成员函数(Method Descriptor)需要包括 返回值参数的数据类型

MethodDescriptor:
(ParameterDescriptor*)ReturnDescriptor      #参数类型 和 返回值类型  *代表0到多个参数
ParameterDescriptor:                        #参数类型的描述就是FieldType
FieldType
ReturnDescriptor:                           #返回值数据类型描述。如果为void 用VoidDescriptor描述,否则也是FieldType
VoidDescriptor:                             #v代表void
V
举个例子:
System.out.print(String str)函数,他的Method Descriptor将是:(Ljava/lang/String;)V

注意:发现没有?Method Descriptor是不包括函数名字的。这么做目的也是为了节省空间。
     因为很多函数可能名字不同,但是Method Descriptor可能是一样的。

常量池实例剖析

直接编译一个Class文件来看看常量池的内容

Java源文件:

/**
 * Created by zhaoyuanchao on 2020/3/16.
 */
public class Person {
    private String name;
    private String job;
    private int age;
    private double money;
    private boolean isBoy;

    public Person() {
    }

    public String getName() {
        return name;
    }

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

    public String getJob() {
        return job;
    }

    public void setJob(String job) {
        this.job = job;
    }

    public int getAge() {
        return age;
    }

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

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }

    public boolean isBoy() {
        return isBoy;
    }

    public void setBoy(boolean boy) {
        isBoy = boy;
    }
}
 Person.java
 1.javac Person.java
 2.javap -verbose Person.class

解析后的Class文件中常量池的内容为:

Constant pool:
  #1 = Methodref          #8.#41         // java/lang/Object."<init>":()V
  #2 = Fieldref           #7.#42         // com/example/glideresource/Person.name:Ljava/lang/String;
  #3 = Fieldref           #7.#43         // com/example/glideresource/Person.job:Ljava/lang/String;
  #4 = Fieldref           #7.#44         // com/example/glideresource/Person.age:I
  #5 = Fieldref           #7.#45         // com/example/glideresource/Person.money:D
  #6 = Fieldref           #7.#46         // com/example/glideresource/Person.isBoy:Z
  #7 = Class              #47            // com/example/glideresource/Person
  #8 = Class              #48            // java/lang/Object
  #9 = Utf8               name
  #10 = Utf8               Ljava/lang/String;
  #11 = Utf8               job
  #12 = Utf8               age
  #13 = Utf8               I
  #14 = Utf8               money
  #15 = Utf8               D
  #16 = Utf8               isBoy
  #17 = Utf8               Z
  #18 = Utf8               <init>
  #19 = Utf8               ()V
  #20 = Utf8               Code
  #21 = Utf8               LineNumberTable
  #22 = Utf8               getName
  #23 = Utf8               ()Ljava/lang/String;
  #24 = Utf8               setName
  #25 = Utf8               (Ljava/lang/String;)V
  #26 = Utf8               getJob
  #27 = Utf8               setJob
  #28 = Utf8               getAge
  #29 = Utf8               ()I
  #30 = Utf8               setAge
  #31 = Utf8               (I)V
  #32 = Utf8               getMoney
  #33 = Utf8               ()D
  #34 = Utf8               setMoney
  #35 = Utf8               (D)V
  #36 = Utf8               ()Z
  #37 = Utf8               setBoy
  #38 = Utf8               (Z)V
  #39 = Utf8               SourceFile
  #40 = Utf8               Person.java
  #41 = NameAndType        #18:#19        // "<init>":()V
  #42 = NameAndType        #9:#10         // name:Ljava/lang/String;
  #43 = NameAndType        #11:#10        // job:Ljava/lang/String;
  #44 = NameAndType        #12:#13        // age:I
  #45 = NameAndType        #14:#15        // money:D
  #46 = NameAndType        #16:#17        // isBoy:Z
  #47 = Utf8               com/example/glideresource/Person
  #48 = Utf8               java/lang/Object
一个常量池的例子,把前面所说的点都穿在了一起。
举个例子说一下吧:第三行

#2 = Fieldref           #7.#42         // com/example/glideresource/Person.name:Ljava/lang/String;

对应前面的数据结构为
    CONSTANT_Fieldref_info{
        u1  tag;
        u2  class_index;
        u2  name_and_type_index;
    }
class_index的索引 指向 #7  为:
#7 = Class              #47            // com/example/glideresource/Person

然后又索引 又指向#47 为:
 #47 = Utf8               com/example/glideresource/Person   实际的具体值
 
 由于name_and_type_index指向代表NameAndType元素的索引。 所以我们看下#42 指到哪里去了:
 果然是NameAndType:
  #42 = NameAndType        #9:#10         // name:Ljava/lang/String;
  
  CONSTANT_NameAndType_info{
      u1    tag;
      u2    name_index;
      u2    descriptor_index;
  }
  
  name_index 的索引为 #9    定点爆破到:
   #9 = Utf8               name
  descriptor_index的索引为 #10  定点爆破到:
  #10 = Utf8               Ljava/lang/String;

field_info 和 method_info

先来看下两种属性的数据结构吧:

field_info{
    u2              access_flags;
    u2              name_index;
    u2              descriptor_index;
    u2              attributes_count;
    attribute_info  attributes[attributes_count];
}

mothod_info{
    u2              access_flags;
    u2              name_index;
    u2              descriptor_index;
    u2              attributes_count;
    attribute_info  attributes[attributes_count];
}

(1) access_flags为访问标志,成员变量和成员函数的访问标志略有不同,下面会介绍到

(2) name_index 为指向成员变量或成员函数名字的Utf8_info常量项

(3) descriptor_index 也指向Utf8_info,分别是描述成员变量的FieldDescriptor和描述成员函数的MethodDescriptor.

(4) attribute 属性信息。

access_flags介绍

在Java中 类、类的成员函数、类的成员变量都有访问=控制设置,比如public private等。 这些都会转换成access_flags

Class的access_flags取值

Field的access_flag取值:

Method的access_flag取值:

属性介绍

属性概貌

属性可用attribute_info数据结构的伪代码表示

attribute_info{
    u2      attribute_name_index;       //属性名称,指向常量池中Utf8常量项的所以
    u4      attribute_length;           //该属性具体内容的长度,即info数组的长度
    u1      info[attribute_length];     //属性的具体内容
}

属性是由其名称来区别的,即attribute_info中的attribute_name_index所指向的Utf8字符串

一些重要的属性名称和它们的作用:

(1)属性的类型由其名字来描述。 比如Code、SourceFile等

(2)不同类型的属性可能出现在ClassFile中不同的成员里

(3)属性也可以包含子属性,比如Code属性能包含LocalVariableTable属性

Code属性

函数的源码转换后得到的字节码就存储Code属性,Code属性的数据结构伪代码如下:

Code_attribute{
    u2      attribute_name_index;
    u4      attribute_length;
    u2      max_stack;
    u2      max_locals;
    u4      code_length;
    u1      code[code_length];
    u2      exception_table_length;
    {
        u2  start_pc;
        u2  end_pc;
        u2  handler_pc;
        u2  catch_type;
    }   exception_table[exception_table_length];
    u2  attributes_count;
    attribute_info  attributes[attributes_count];
}

attribute_name_index: 指向内容为Code的Utf8常量项。attribute_length表示接下来内容的长度

max_stack: JVM执行一个指令的时候,该指令的操作数存储在一个名叫"操作数栈"的地方,每一个操作数占用一个或两个栈顶。stack就是一块只能进行先入后出的内存。max_stack用于说明这个函数在执行过程中需要最深多少栈的空间。

max_locals: 表示该函数最多包括几个局部变量。

code_length 和 code: code 存储java编译后的指令码,code_length表明其长度。

exception_table_length 和 exception_table: 一个函数可以包含多个try/catch语句,一个try/catch语句对应exception_table数组中的一项。

\color{red}{JVM执行的时候,会维护一个变量来指向当前要执行的指令,这个变量就叫 pc}

start_pc: 描述try语句从哪条指令开始。 end_pc: 表示这个try语句到哪条指令结束 注意不包含catch

handler_pc: 表示catch语句从哪条指令开始

catch_type: 表示catch中截获的Exception或Error的名字,指向Utf8_info常量项。

另外Code_atrribute还可能包含其他属性:
(1) LineNumberTable:用于调试,比如指明哪条指令。对应源码的哪一行。
(2) LocalVariableTable:用于调试,可以用来计算本地变量的值。
(3) LocalVariableTypeTable: 功能和LocalVariableTable类似。

举个例子(为了凸显行号我直接截图了)

对应生成的Code属性如下:

{
  public com.example.glideresource.Sample();
    descriptor: ()V
    flags: 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 getTitle();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field title:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 11: 0

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

  public int getAge();
    descriptor: ()I
    flags: 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: 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
}

以getTitle()方法为例子:
descriptor:()Ljava/lang/String;                 #返回值为String类型的无参方法
flags:ACC_PUBLIC                                #access_flags 为Public
stack = 1,  locals = 1,args_size = 1            #函数栈空间需要1
                                                #包含一个局部变量
LineNumberTable:
    line 11:0                                   #对应start_pc 对应源码11行 retuen title;

《深入理解Android Java虚拟机ART》这本书应该是我买过最厚的一本android书了。看书整理笔记也是一个学习和巩固知识的途径吧,我不知道这本书我可以坚持更多少篇博客,但是尽可能坚持下去吧。提升也是个积累的过程。

下一篇-关于Dex的文件格式 待续