playground

类文件结构

在学习类文件结构之前我们先编写一个简单的类。

package playground;

public class Test {

    private int m;

    public int inc() {
        return m + 1;
    }
}

对以上的类使用javac进行编译后我们得到一个.class文件,它的十六进制表示如下。

CA FE BA BE 00 00 00 36 00 13 0A 00 04 00 0F 09 00 03 00 10 07 00 11 07 00 12 01 00 01 6D 01 00 01 49 01 00 06 3C 69 6E 69
74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 03 69 6E 63 01 00
03 28 29 49 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 09 54 65 73 74 2E 6A 61 76 61 0C 00 07 00 08 0C 00 05 00 06 01 00
0F 70 6C 61 79 67 72 6F 75 6E 64 2F 54 65 73 74 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 00 21 00 03 00 04
00 00 00 01 00 02 00 05 00 06 00 00 00 02 00 01 00 07 00 08 00 01 00 09 00 00 00 1D 00 01 00 01 00 00 00 05 2A B7 00 01 B1
00 00 00 01 00 0A 00 00 00 06 00 01 00 00 00 03 00 01 00 0B 00 0C 00 01 00 09 00 00 00 1F 00 02 00 01 00 00 00 07 2A B4 00
02 04 60 AC 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 00 08 00 01 00 0D 00 00 00 02 00 0E

结构

从整体上看,一个Class文件结构如下。

长度(字节) 4 2 2 2 不定 2 2 2 2 2 2 不定 2 不定 2 不定
名称 magic minor_version major_version constant_pool_count constant_pool access_flags this_class super_class interfaces_count interfaces fields_count fields methods_count methods attributes_count attributes

类文件非常紧凑,每个数据项之间没有分隔符。数据项有2种类型,一种是无符号数,另一种称为表。无符号数可以表示数字或者UTF-8编码的字符串,比如上面的magic项,就是一个占4个字节的无符号数;表是由多个无符号数和其它表组成的复合结构数据。表中的数据项数量通常是不定的,通常在表前面有一个前置的计数器记录表中数据项的个数,比如上图中的constant_pool表,它的数据项数量记录在前面的constant_pool_count中。

魔数(magic)

魔数是指开头的4个字节CA FE BA BE,它的唯一作用是用来表示这是一个Class文件。

版本(minor_version、major_version)

之后的4个字节minor_versionmajor_version分别是次版本号和主版本号,表示Class文件的版本,版本号从45.0开始。例子中的版本是0x00000036,也就是十进制的54,也就是JDK10。

常量池(constant_pool_count、constant_pool)

版本号之后的2个字节代表常量池的容量,这里是0x0013,即十进制的19。比较特殊的是,这里的计数是从1开始的,实际数据项数目需要在计数上减去1,因此这里的19表示常量池中有18个常量。当计数为0时,则有特殊含义,表示不引用任何一个常量池项目。

常量池中的每一项都是一个表,目前一共有十几种不同的数据结构类型,它们的通用结构如下。

长度(字节) 1 len1 lenN
名称 tag property1 propertyN

比如CONSTANT_Class_info类型的结构如下。

长度(字节) 1 1
名称 tag name_index

每一个类型开头的第一个字节都是tag,代表当前常量的类型,比如上面的CONSTANT_Class_info类型,它的tag就是7,除了tag外,后面的结构每种类型都有不同。下面列出部分类型和tag的关系。

类型 标志
CONSTANT_Utf8_info 1
CONSTANT_Integer_info 3
CONSTANT_Float_info 4
CONSTANT_Long_info 5
CONSTANT_Double_info 6
CONSTANT_Class_info 7
CONSTANT_String_info 8
CONSTANT_Fieldref_info 9
CONSTANT_Methodref_info 10
CONSTANT_InterfaceMethodref_info 11
CONSTANT_NameAndType_info 12

0x0013之后紧跟着的字节是0x0A,即十进制的10,按照上面表格中的对应关系,我们可以看到第一个常量的类型是CONSTANT_Methodref_info,它的结构如下。

长度 1 2 2
名称 tag index index
描述 10 指向CONSTANT_Class_info类型常量的索引 指向CONSTANT_NameAndType类型常量的索引

因此后面的两个值分别是0x00040x000F,即十进制的415

第4个常量的十六进制是07 00 12(这里的索引是从1开始的,1就是指第1个常量而不是第0个常量),因此它是CONSTANT_Class_info类型的常量。

CONSTANT_Class_info类型的结构如下。

长度 1 2
名称 tag index
描述 7 指向全限定名常量项的索引

因此0x0012,即十进制的18。第18个常量的十六进制是01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74,它的标志是0x01,即十进制的1,因此是一个CONSTANT_Utf8_info类型的常量,该类型的结构如下。

长度 1 2 length
名称 tag length bytes
描述 1 UTF-8编码的字符串占用的字节数 长度为length的UTF-8编码的字符串

所以该字符串的字节数是0x0010,即十进制的16,内容是6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74,用UTF-8解码后就是java/lang/Object

我们再来看第15个常量,它的十六进制是0C 00 07 00 080x0C的十进制是12,因此它是一个CONSTANT_NameAndType_info类型的常量,其结构如下。

长度 1 2 2
名称 tag index index
描述 12 指向该字段或方法名称常量的索引 指向该字段或方法描述符常量的索引

后面的两个数据项的值0x00070x0008分别是十进制的78。第7和第8个常数项的十六进制表示分别是01 00 06 3C 69 6E 69 74 3E01 00 03 28 29 56,两者都是CONSTANT_Utf8_info类型的常量,它们的字符串值分别是<init>()V

综上,我们把这个CONSTANT_Methodref_info类型的变量连起来看,就是java/lang/Object<init>()V

常量池中其余的常量也可以按照上面分析的步骤计算出来,JDK自带了一个工具javap可以帮我们自动解析,最后的结果如下。

// 编译后执行 javap -v Test。
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // playground/Test.m:I
   #3 = Class              #17            // playground/Test
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               Test.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               playground/Test
  #18 = Utf8               java/lang/Object

访问标志(access_flags)

之后的2个字节是00 21,表示访问标志。访问标志共有8个,如下所示。

名称 描述
ACC_PUBLIC 0x0001 是否为Public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可以设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义
ACC_INTERFACE 0x0200 标志这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标志这是一个注解
ACC_ENUM 0x4000 标志这是一个枚举
本例中Test类的标志是ACC_PUBLIC ACC_SUPER,也就是0x0001 0x0020,等于0x0021

类索引(this_class)

类索引用于确定当前类的全限定名,它指向一个CONSTANT_Class_info类的常量,本例中它的值是0x0003,也就是十进制的3,第3个常量的值是07 00 11,它继续指向一个CONSTANT_Utf8_info的常量,它的值是01 00 0F 70 6C 61 79 67 72 6F 75 6E 64 2F 54 65 73 74,其字符串表示是playground/Test

父类索引(super_class)

父类索引用于确定当前类的父类的全限定名,除了java.lang.Object之外所有的类都有父类。在本例中父类索引的值是0x0004,也就是十进制的4,第4哥常量的值是07 00 12,它继续指向一个CONSTANT_Utf8_info的常量,它的值是01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74,其字符串表示是java/lang/Object

接口索引集合(interfaces_count、interfaces)

接口索引集合用来描述当前类实现了哪些接口。本例中的类没有实现接口,因此接口计数器部分(interfaces_count)的值为00x0000)。

字段表集合(fields_count,fields)

字段表用于描述接口或类中的变量。变量可以是静态变量或成员变量,但不包括方法的局部变量。字段表计数器(fields_count)表示字段表中包含多少个字段,本例中的值是0x0001,也就是1个。

一个字段表的结构如下所示。

长度(字节) 2 2 2 2 不定
名称 access_flags name_index descriptor_index attributes_count attributes

访问标志,比如ACC_PUBLIC(0x0001)表示字段是否是公开的,ACC_PRIVATE(0x0002)表示字段是否是私有的等。本例中该数据项的值是0x0002,表示私有字段。

指向一个CONSTANT_Utf8_info类型的常量,代表字段的简单名称。简单名称是指仅有字段的名字,而不包括返回值、修饰符等内容。本例中该数据项的值是0x0005,指向第5个常量01 00 01 6D,它的字符串表示是m

指向一个CONSTANT_Utf8_info类型的常量,代表字段的描述符。本例中该数据项的值是0x0006,指向第6哥常量01 00 01 49,它的字符串表示是I

字段可以包含一些额外的属性,attributes_count是属性计数器,表示属性的个数,本例中没有额外的属性,因此attributes_count的值是0x0000,即0

字段包含的额外的属性,本例中没有额外的属性。

描述符

上面多次提到了描述符,那么什么是描述符呢?

描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据规则,基本数据类型使用一个大写字母来表示,如下所示。

字符 类型
B byte
C char
D double
F float
I int
J long
S short
Z boolean
V void

对于对象类型则用字符L加上对象的全限定名来表示,比如Ljava/lang/Object,并且加上一个分号表示表示全限定名结束。对于数组类型,每多一个维度就用一个[字符描述,比如String[][]类型的描述符是[[Ljava/lang/String;int[]类型的描述符是[I

上面是描述字段的方式,当我们用描述符来描述方法时,按照先参数列表后返回值的顺序进行描述,参数列表放在一对括号中,比如本例中方法inc()的描述符是()I,由于没有入参,因此括号中为空,括号后的I表示返回值是int类型。又比如Integer valueOf(String s, int radix)方法,它的描述符是(Ljava/lang/String;I)Ljava/lang/Integer;

方法表集合(methods_count,methods)

方法表和上面的字段表类似,只不过是用来表述方法的。方法表计数器(methods_count)表示方法表中方法的数量,这里的值是0x0002,表示有2个方法。

下面,我们来看下方法表的结构。

长度(字节) 2 2 2 2 不定
名称 access_flags name_index descriptor_index attributes_count attributes

从结构上看它和字段表完全一致,每个字段的含义也是相同的。

我们先来看方法表中的第一个方法,它各个数据项如下所示。

访问标志,和字段表的区别是值不同,比如方法不能被volatile修饰,因此没有ACC_VOLATILE标志,但是针对方法有ACC_SYNCHRONIZED标志表示synchronized而字段没有。本例中access_flags的值是0x0001,表示public

名称索引。本例中name_index的值是0x0007,指向第7个常量,它的值是01 00 06 3C 69 6E 69 74 3E,是一个CONSTANT_Utf8_info类型的常量,其字符串表示是<init>

描述符索引,本例中的值是0x0008,指向第8个常量,它的值是01 00 03 28 29 56,是一个CONSTANT_Utf8_info类型的常量,其字符串表示是()V

属性计数器。这里的值是0x0001,表示有一个属性。

属性表。这里存放的是各个属性名称的索引,这里的值是0x0009,指向第9个常量,它的值是01 00 04 43 6F 64 65,是一个CONSTANT_Utf8_info类型的常量,其字符串表示是Code,也就是说该方法有一个名为Code的属性。关于属性的介绍会在下面具体展开。

CONSTANT_Fieldref_info类型常量和方法表

既然我们已经有了方法表,那么常量池中CONSTANT_Fieldref_info类型的常量有什么用呢?是不是有点多余呢?

实际上这两者的用处是不同的,方法表记录的是字段本身的信息,而只有当我们引用到某个字段时才会使用到CONSTANT_Fieldref_info类型的常量。那么什么时候才算引用到该字段呢?比如Test类中的inc()方法就引用到了字段m,最常见的还有各个字段的GetterSetter方法。

public int inc() {
    return m + 1;
}

属性表

在字段表和方法表都出现了属性表的概念,下面我们看一下属性表到底是什么。

属性表用于描述一些额外的信息,各个属性之间不严格有序,只要不与已有的属性重名,任何属性都可以写入到属性表中,当然,虚拟机会忽略它不认识的属性,因此我们可以把属性表可以看成是一个Map对象。

属性表的结构如下。

长度(字节) 2 4 不定
名称 attribute_name_index attribute_length info

其中attribute_name_indexattribute_length部分是所有属性都有的,而info部分是每个属性自定义的。

在上文中提到方法<init>有一个Code属性,它存储了方法运行时执行的字节码,以下是它的结构。

长度(字节) 2 4 2 2 4 不定 2 不定 2 不定
名称 attribute_name_index attribute_length max_stack max_locals code_length code exception_table_length exception_table attributes_count attributes

属性名称。指向一个CONSTANT_Utf8_info类型的常量,最后解析出的字符串值是Code

属性长度。这里的值是0x0000001D,即29

操作数栈的最大深度。这里是0x0001,即1

局部变量表所需的存储空间,它的单位是Slot,是虚拟机为局部变量分配内存时用到的最小单位,对于不超过32位的数据类型占用1个Slot,比如boolean,byte,char,float,int等;对于超过32位的数据类型则占2个Slot,比如longdouble。 局部变量表所需的存储空间并不等于局部变量的个数,因为当代码执行的位置超过一个局部变量的作用域时,该局部变量的Slot就可以被重用。这里的值是0x0001,即1

生成的字节码的长度。这里的值是0x00000005,即5。需要注意的是,虽然它占4个字节,但是虚拟机规范中规定一个方法不允许超过65535条字节码指令,因此实际上只用到了2个字节。

方法的字节码指令。字节码指令并不是对常量池中常量的引用,当虚拟机读取字节码时,它可以找出对应的字节码指令,并且可以知道该指令后面是否需要跟随参数。 由于每个字节码指令只占1个字节,因此该方法的字节码指令的值是2A B7 00 01 B1。虚拟机依次读取后就可以得到以下的字节码。

步骤 字节码 字节码指令 描述
1 0x2A aload_0 将第0个Slot中引用类型的局部变量放入操作数栈顶。
2 0xB7 invokespecial 调用操作数栈顶引用类型变量的构造方法,后面的2个字节00 01是它接收的参数,指向常量池中CONSTANT_Methodref_info类型的常量,这里就是本文刚开始解析的java/lang/Object."<init>":()V方法。
3 0xB1 return 结束当前方法。

我们用javap工具查看测试类中的两个方法。

 public playground.Test();
    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 3: 0

  public int inc();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 8: 0

结果中显示两个放的局部变量数量locals和方法参数数量args_size都是1,但是这两个方法并没有传入任何参数。实际上这里有一个隐藏的this参数,编译器在编译时会把对this的访问转化成对一个普通方法参数的访问。

异常表的长度。本例中没有捕获异常因此异常表长度为0,

异常表,详情见异常

属性计数器。从这里我们可以看到,属性里面还可以嵌套属性,这里属性计数器的值是0x0001,表示Code属性中有1个子属性。

属性。这里开头的两个字节是00 0A,因此指向第10个常量,第10个常量的值是01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65,它是一个CONSTANT_Utf8_info类型的常量,最后解析出的字符串值是LineNumberTable,该属性用于描述源码行号和字节码行号之间的对应关系,其结构如下。

长度(字节) 2 4 2 不定
名称 attribute_name_index attribute_length line_number_table_length line_number_table

其中attribute_name_indexattribute_lengthCode属性类似,这里不再赘述,后面两个数据项是LineNumberTable特有的。

行号表计数器,这里的值是0x0001,也就是1

行号表,记录了源码的行号和字节码行号的关系。每一对关系占4个字节,包括2个字节的start_pc和2个字节的line_number数据项,前者是字节码行号,后者是源码行号。这里的值是00 00 00 03,表示第0行字节码对应源码中的第3行。

接下来还剩下一部分的字节我们还没讲解,实际上它就是方法表集合中第二个方法inc()的相关数据,和我们分析的第一个方法类似,不再这里展开。

属性表集合(attributes_count,attributes)

最后我们迎来了类文件的尾声——属性表集合,它在上面的方法和字段部分已经出现过。

开头的00 01表示属性计数器attributes_count的值是1,即只有1个属性。后面的两个字节00 0D指向第13个常量,该常量是一个CONSTANT_Utf8_info类型的常量,解析出的字符串值是SourceFile,该类型的属性用于记录生成当前类文件的源码文件名称,它的结构如下。

长度(字节) 2 4 2
名称 attribute_name_index attribute_length sourcefile_index

sourcefile_index的值是0x000E,指向常量池中第14个常量,它的类型是CONSTANT_Utf8_info,解析出的字符串值是Test.java

其它属性

除了上面提到的CodeLineNumberTable属性,还有几下几种常见的属性需要了解。

Exceptions是和Code同级的属性,用来描述方法中可能抛出的受检查异常。

该属性用于记录局部变量的名字(方法参数名也算局部变量)。虚拟机在执行字节码指令时是不关注变量名的,因此如果没有LocalVariableTable属性,当别人在引用这个方法时,参数名称就会丢失,有时会显示成IDE生成的args0等形式的参数名。这项属性需要在编译时加上-g参数才会生成。

比如有以下的代码。

public class Test {

    public int inc(int number) {
        return number + 1;
    }
}

我们使用javac -g Test.java命令进行编译,然后用javap -l Test查看生成的字节码信息。

public int inc(int);
    LineNumberTable:
      line 6: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       4     0  this   Lplayground/Test;
          0       4     1 number   I

可以看到这里有两个局部变量,一个是隐藏的this,一个就是方法参数名numberStartLength分别代表局部变量生命周期开始的字节码行号和范围长度,也就是它的作用域范围。

该属性的作用是通知虚拟机自动为静态变量初始化。

目前虚拟机有2种策略,一种是在类构造器<clinit>方法中对静态变量进行初始化赋值,第二种就是使用ConstantValue属性。

对于第2种策略也有几种不同的情况。

以下代码会生成ConstantValue属性。

public final static int VALUE = 10;

以下代码则不会生成ConstantValue属性。

public static int VALUE = 10;

实际上只有当静态变量是基本类型或String类型并且被final修饰时才会使用到ConstantValue属性。

因此以下代码也无法使用ConstantValue属性,因为它是引用类型且不是String

public final static Object VALUE = new Object();

之所以引用类型不能使用ConstantValue属性是因为ConstantValue属性的值是对常量池中常量的引用,而常量池中没有通用的引用类型,只有CONSTANT_Integer_info等特定的几种类型。

该属性用于记录内部类和宿主类之间的关系。

@Deprecated注解标注的类、字段或方法拥有Deprecated属性,表示不推荐使用。

该属性表示一个类、字段或方法是由编译器自动产生的,而不是由源码直接产生的。需要注意的是<init><clinit>方法除外。

该属性用于提升字节码验证的性能。

用于存储类或字段的泛型信息。以下代码中的泛型信息就是从Signature属性中获取的。

public class Test {

    List<Integer> list = new ArrayList<>();

    public static void main(String[] args) throws NoSuchFieldException {
        ParameterizedType type = (ParameterizedType) Test.class.getDeclaredField("list").getGenericType();
        System.out.println(type.getActualTypeArguments()[0]);// 此处打印“class java.lang.Integer”。
    }
}