Class文件格式

本文系《深入理解Java虚拟机》总结

ClassFile{
    u4               magic;
    u2               minor_version;
    u2               major_version;
    u2               constant_pool_count;
    cp_info          constant_pool[constant_pool_count-1];
    u2               access_flags;
    u2               this_class;
    u2               super_class;
    u2               interface_count;
    u2               interfaces[interface_count];
    u2               fields_count;
    field_info       fields[fields_count];
    u2               methods_count;
    method_info      methods[methods_count];
    u2               attribute_count;
    attribute_info   attributes[attribute_count];
}
  • Magic 魔数 固定值 0xCAFEBABE
  • minor_version 副版本号
  • major_version 主版本号
  • constant_pool_count 常量池计数器
  • constant_pool[] 常量池
  • access_flags 访问标志
  • this_class 类索引
  • super_class 父类索引
  • interface_count 接口计数器
  • interfaces[] 接口表
  • fields_count 字段计数器
  • fields[] 字段表
  • methods_count 方法计数器
  • methods[] 方发表
  • attribute_count 属性计数器
  • attributes[] 属性表

通过自己写的一个Java类编译来分析下

Java代码:

public class SimpleClazz {

    private int a = 3;

    public void add() {
        int i = 0;
        i++;
    }

}

Javap -verbose

public class com.fcs.test.SimpleClazz
  SourceFile: "SimpleClazz.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#18         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#19         //  com/fcs/test/SimpleClazz.a:I
   #3 = Class              #20            //  com/fcs/test/SimpleClazz
   #4 = Class              #21            //  java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/fcs/test/SimpleClazz;
  #14 = Utf8               add
  #15 = Utf8               i
  #16 = Utf8               SourceFile
  #17 = Utf8               SimpleClazz.java
  #18 = NameAndType        #7:#8          //  "<init>":()V
  #19 = NameAndType        #5:#6          //  a:I
  #20 = Utf8               com/fcs/test/SimpleClazz
  #21 = Utf8               java/lang/Object
{
  public com.fcs.test.SimpleClazz();
    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_3
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 6: 0
        line 8: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      10     0  this   Lcom/fcs/test/SimpleClazz;

  public void add();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iinc          1, 1
         5: return
      LineNumberTable:
        line 11: 0
        line 12: 2
        line 13: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       6     0  this   Lcom/fcs/test/SimpleClazz;
               2       4     1     i   I
}

class真身:

CAFEBABE0000003300160A00040012090003001307001407001501000161010001490100063C696E69743E010003282956010004436F646501000F4C696E654E756D6265725461626C650100124C6F63616C5661726961626C655461626C650100047468697301001A4C636F6D2F6663732F746573742F53696D706C65436C617A7A3B0100036164640100016901000A536F7572636546696C6501001053696D706C65436C617A7A2E6A6176610C000700080C00050006010018636F6D2F6663732F746573742F53696D706C65436C617A7A0100106A6176612F6C616E672F4F626A65637400210003000400000001000200050006000000020001000700080001000900000038000200010000000A2AB700012A06B50002B100000002000A0000000A00020000000600040008000B0000000C00010000000A000C000D00000001000E000800010009000000420001000200000006033C840101B100000002000A0000000E00030000000B0002000C0005000D000B00000016000200000006000C000D000000020004000F0006000100010010000000020011

魔数、版本号

魔数就是4个字节的0xCAFEBABE,cafebabe

版本号分为副版本号和主版本号

如上图,副版本号为0x0000,主版本号为0x0033,即十进制的51,代表JDK1.7

常量池

首先是常量池的count 为u2类型 常量池的大小是1~count-1
然后是常量池具体内容 这是一个集合结构 不管什么类型的常量
tag都是u1类型 根据tag就可以找到具体常量类型 然后得到具体结构

00 16 0A 00 04 00 12 09 00 03 00

可得有21个常量 然后就是常量池内容
0A代表这是一个CONSTANT_Methodref_info类型的常量,知道其类型就知道了它的结构,然后它在字节码中就有了固定的长度。

tag u1
index u2 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项
index u2 指向字段描述符CONSTANT_NameAndType的索引项

根据上面编码可得 index=4 index=18

接着是第二个常量 tag=9 代表CONSTANT_Fieldref_info类型的常量 找到其结构可锁定其在字节码中的长度 就这样依次可得到所有常量

访问标志

00 21 00 03 00 04 ….

紧接着就是access_flags
access_flags中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志
位要求一律为0。以代码清单6-1中的代码为例,TestClass是一个普通Java类,不是接口、枚举或者注解,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK 1.2之后的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM这6个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。

可以看出,access_flags标志(偏移地址:0x000000EF)的确为0x0021。

类索引、父类索引和接口索引集合

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集
合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。对于接口索引集合,入口的第一项——u2类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。

00 03 00 04 00 00 .. ..

可以看出该类索引指向第3个常量 内容为com/fcs/test/SimpleClazz
父类索引指向第4个常量 为java/lang/Object
接口索引集合中计数器值为0 所以没有实现任何接口

字段表集合

00 01 00 02 00 05 00 06 00 00 00 02

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以
及实例级变量,但不包括在方法内部声明的局部变量。

字段表结构
u2 access_flag
u2 name_index
u2 descriptor_index
u2 attributes_count
attribute_info attributes

字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型。

跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。

描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。

对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为:“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录为“[I”。

如上
第一个u2类型的数据为容量计数器fields_count 0x0001 表示这个类只有一个字段表数据
access_flag 为 0x0002 代表private修饰符的ACC_PRIVATE标志位为真
name_index 索引指向第5个常量 是一个字符串a
descriptor_index 索引指向第6个常量 是描述符I 代表int类型
attributes_count 为0x0000 所以不再描述

方法表

方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项,见表6-11。这些数据项目的含义也非常类似,仅在访问标志和属性表集合的可选项中有所区别。

00 02 00 01 00 07 00 08 00 01 00 09

0x0002表示有两个方法,包括编译器添加的实例构造器<init>和源码中
的方法add()
access_flag 为0x0001 同样表示private
name_index 索引指向常量池中第7个常量
descriptor_index 索引指向第8个常量 ()V 表示返回值是void类型
attributes_count的值为0x0001就表示此方法的属性表集合有一项属性,属性名称索引为0x0009,对应常量为“Code”,说明此属性是方法的字节码描述。

属性表

在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占
用的位数即可。

u2 attribute_name_index
u4 attribute_length
u1 info

  • Code属性
    Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性
    内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬
    如接口或者抽象类中的方法就不存在Code属性。

Code属性表结构
u2 attribute_name_index 固定值Code
u4 attribute_length
u2 max_stack 操作数栈深度的最大值
u2 max_locals 局部变量所需的存储空间
u4 code_length
u1 code
u2 exception_table_length
exception_info exception_table 异常表
u2 attributes_count
attribute_info attributes

Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。

00 02 00 01 00 00 00 0A 2A B7 00 01 2A 06 B5 00 02 B1

max_stack 0x0002 操作数栈最大深度为2
max_locals 0x0001 局部变量所需要的存储空间(单位Slot) 为1
code_length 0x0000000A 字节码长度为10
code 2A B7 00 01 2A 06 B5 00 02 B1

1)读入2A,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为
reference类型的本地变量推送到操作数栈顶。

2)读入B7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的
reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。

3)读入00 01,这是invokespecial的参数,查常量池得0x0001对应的常量为实例构造
器“<init>”方法的符号引用。

4)读入2A,同样对应的指令为aload_0,将第0个Slot中为reference类型的本地变量推送到操作数栈顶。

5)读入06,查表得0x06对应的指令为iconst_3,含义是将int型3推送至栈顶。

5)读入B5,putfield,含义是为指定类的实例域赋值,后面两个字节应该对应具体的实例域。

5)读入00 02,指向常量池中的 com/fcs/test/SimpleClazz.a:I,说明是给a赋值。

9)读入B1,查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为void。
这条指令执行后,当前方法结束。


exception_info

00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 06 00 04 00 08 00 0B 00

然后是两个字节的code_lenght,为0x0000表示异常表长度为0,即没有异常块。
接着又是两个字节的 attributes_count ,说明存在2个属性表类型的结构。
0x000A attribute_name_index指向的是常量池中的第10个常量LineNumberTable

  • LineNumberTable属性
    LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。

LineNumberTable属性结构
u2 attribute_name_index
u4 attribute_length
u2 line_number_table_length
line_number_info line_number_table

00 00 00 0A 00 02 00 00 00 06 00 04 00 08

0x0000000A代表的attribute_length为10,
0x0002代表 line_number_table_length为2

line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。

字节码行号为0(0x0000) 对应Java源码行号为6(0x0006)
字节码行号为4(0x0004) 对应Java源码行号为8(0x0008)

接下来是第二个属性表结构

00 0B 00 00 00 0C

attribute_name_index为11,指向常量池中第11项LocalVariableTable

  • LocalVariableTable属性

LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中。

u2 attribute_name_index
u4 attribute_length
u2 local_variable_table_length
local_variable_info local_variable_table

0x0000000C 表示attribute_length为12

00 01 00 00 00 0A 00 0C 00 0D 00 00

0x0001表示local_variable_table_length为1
0x0000表示start_pc为0
0x000A表示length为10
0x000C表示name_index为12,为this
0x000D表示descriptor_index为13,为Lcom/fcs/test/SimpleClazz;
0x0000表示index为0

local_variable_info项目结构
u2 start_pc
u2 length
u2 name_index
u2 descriptor_index
u2 index

start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。

name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。

index是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时(double和long),它占用的Slot为index和index+1两个。

在JDK 1.5引入泛型之后,LocalVariableTable属性增加了一个“姐妹属性”:
LocalVariableTypeTable,这个新增的属性结构与LocalVariableTable非常相似,仅仅是把记录的字段描述符的descriptor_index替换成了字段的特征签名(Signature),对于非泛型类型来说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉[3],描述符就不能准确地描述泛型类型了,因此出现了LocalVariableTypeTable。

接着分析方法表中的第二个方法

00 01 00 0E 00 08 00 01 00 09

access_flag 为0x0001 同样表示private
name_index 索引指向常量池中第14个常量,是utf类型常量“add”,表示add方法名
descriptor_index 索引指向第8个常量 ()V 表示返回值是void类型
attributes_count的值为0x0001就表示此方法的属性表集合有一项属性,属性名称索引为0x0009,对应常量为“Code”,说明此属性是方法的字节码描述。

此处分析与上面的构造方法大同小异。

总结:

  1. Class文件的格式代表的是一种规范,不管你的Java类是什么样的,按照这种规范去编译,得到的class文件总能被虚拟机所接受。

  2. 类的大小和内容都是不固定的,但是我们可以通过称为表的结构来表示这种复杂的内容,表中还可以套表,类似对象中包含对象一样。

  3. Class文件看似杂乱无章,其实有着Java虚拟机规范的约束,很容易翻译成虚拟机理解的内容。Java类只是为了方便我们理解和使用,这也就是所谓的高级之处。

  4. 从最外层看,Class文件有着固定的结构,从魔数,常量池到属性表,中间穿插了几种表,表中再来个表,这样显得非常灵活,不管有多少个表,顺着找下去,总有尽头,总有固定的字节数,我觉得解析的过程类似代码的执行顺序,遇到其他的类或者方法,就直接跳到那个去,执行完再回来。通过count和info这两个结构,化无限为有限。

原文地址:https://www.cnblogs.com/lucare/p/8679139.html