Java Class文件结构解析

Java代码必须要被编译成class文件后,虚拟机才能够加载运行,要搞清楚Java的类加载机制,首先必须要理解Class文件的内部结构。
本文参考了周志明所著《深入理解java虚拟机》一书,并结合自身实践而写。

1.Class文件基本结构概述

  • Class文件是一组以8位字节为基础单位的二进制流,当遇到需要8位字节以上空间的数据项时,则会按照高位在前的方式分隔成若干个8位字节进行存储。
  • Class文件由无符号数构成。
  • 无符号数:以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节和8个字节的无符号数,可以用来描述数字、索引引用、数量值、按照UTF-8编码构成的字符串值。
  • :由多个无符号数或其他表作为数据项构成的复杂数据类型,所有表都习惯性地以“_info”结尾。

Class文件格式如下:

类型 描述 备注
u4 magic 魔数:0xCAFEBABE
u2 minor_version 小版本号
u2 major_version 主版本号
u2 constant_pool_count 常量池大小,从1开始
cp_info constant_pool[constant_pool_count - 1] 常量池信息
u2 access_flags 访问标志
u2 this_class 类索引
u2 super_class 父类索引
u2 interfaces_count 接口个数
u2 interfaces[interfaces_count] 接口类索引信息
u2 fields_count 字段数
field_info fields[fields_count] 字段表信息
u2 methods_count 方法数
method_info methods[methods_count] 方法表信息
u2 attributes_count 属性个数
attribute_info attributes[attributes_count] 属性表信息

2.一个典型的Class文件

//接口类
public interface Car {
    void drive();
}

//实现类
public class BMWCar implements Car{

    private String name;
    
    public BMWCar() {
        name = "宝马";
    }
    
    @Override
    public void drive() {
        System.out.println("BMW car drive." + name);
    }
    
}

采用javac命令编译以上代码后,用Sublime编辑器打开BMWCar.class文件,内容如下:

cafe babe 0000 0034 002f 0a00 0c00 1708
0018 0900 0b00 1909 001a 001b 0700 1c0a
0005 0017 0800 1d0a 0005 001e 0a00 0500
1f0a 0020 0021 0700 2207 0023 0700 2401
0004 6e61 6d65 0100 124c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b01 0006 3c69
6e69 743e 0100 0328 2956 0100 0443 6f64
6501 000f 4c69 6e65 4e75 6d62 6572 5461
626c 6501 0005 6472 6976 6501 000a 536f
7572 6365 4669 6c65 0100 0b42 4d57 4361
722e 6a61 7661 0c00 1000 1101 0006 e5ae
9de9 a9ac 0c00 0e00 0f07 0025 0c00 2600
2701 0017 6a61 7661 2f6c 616e 672f 5374
7269 6e67 4275 696c 6465 7201 000e 424d
5720 6361 7220 6472 6976 652e 0c00 2800
290c 002a 002b 0700 2c0c 002d 002e 0100
0642 4d57 4361 7201 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 0343 6172
0100 106a 6176 612f 6c61 6e67 2f53 7973
7465 6d01 0003 6f75 7401 0015 4c6a 6176
612f 696f 2f50 7269 6e74 5374 7265 616d
3b01 0006 6170 7065 6e64 0100 2d28 4c6a
6176 612f 6c61 6e67 2f53 7472 696e 673b
294c 6a61 7661 2f6c 616e 672f 5374 7269
6e67 4275 696c 6465 723b 0100 0874 6f53
7472 696e 6701 0014 2829 4c6a 6176 612f
6c61 6e67 2f53 7472 696e 673b 0100 136a
6176 612f 696f 2f50 7269 6e74 5374 7265
616d 0100 0770 7269 6e74 6c6e 0100 1528
4c6a 6176 612f 6c61 6e67 2f53 7472 696e
673b 2956 0021 000b 000c 0001 000d 0001
0002 000e 000f 0000 0002 0001 0010 0011
0001 0012 0000 002b 0002 0001 0000 000b
2ab7 0001 2a12 02b5 0003 b100 0000 0100
1300 0000 0e00 0300 0000 0600 0400 0700
0a00 0800 0100 1400 1100 0100 1200 0000
3900 0300 0100 0000 1db2 0004 bb00 0559
b700 0612 07b6 0008 2ab4 0003 b600 08b6
0009 b600 0ab1 0000 0001 0013 0000 000a
0002 0000 000c 001c 000d 0001 0015 0000
0002 0016 

3.魔数

Java Class文件结构解析_第1张图片
magic

4.版本号

Java Class文件结构解析_第2张图片
version

次版本号是:0000
主版本号是:0034,十进制是52,表示采用的是jdk1.8

5.常量池

版本号后面是常量池,常量池中常量的数量是不固定的,所以常量池的入口处有一个u2类型的数据,表示常量池中常量的数值大小。
Java Class文件结构解析_第3张图片
constant_pool_count

0x002f十进制数值是47,表示常量池常量数为46(注意常量池计数是从1而不是0开始),使用“javap -v BMWCar”命令可以查看Class文件的信息如下:

  Last modified 2017-11-10; size 644 bytes
  MD5 checksum ac6d7477d45479490e4ea3f660b1dcdd
  Compiled from "BMWCar.java"
public class BMWCar implements Car
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#23        // java/lang/Object."":()V
   #2 = String             #24            // 宝马
   #3 = Fieldref           #11.#25        // BMWCar.name:Ljava/lang/String;
   #4 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Class              #28            // java/lang/StringBuilder
   #6 = Methodref          #5.#23         // java/lang/StringBuilder."":()V
   #7 = String             #29            // BMW car drive.
   #8 = Methodref          #5.#30         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #9 = Methodref          #5.#31         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = Methodref          #32.#33        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Class              #34            // BMWCar
  #12 = Class              #35            // java/lang/Object
  #13 = Class              #36            // Car
  #14 = Utf8               name
  #15 = Utf8               Ljava/lang/String;
  #16 = Utf8               
  #17 = Utf8               ()V
  #18 = Utf8               Code
  #19 = Utf8               LineNumberTable
  #20 = Utf8               drive
  #21 = Utf8               SourceFile
  #22 = Utf8               BMWCar.java
  #23 = NameAndType        #16:#17        // "":()V
  #24 = Utf8               宝马
  #25 = NameAndType        #14:#15        // name:Ljava/lang/String;
  #26 = Class              #37            // java/lang/System
  #27 = NameAndType        #38:#39        // out:Ljava/io/PrintStream;
  #28 = Utf8               java/lang/StringBuilder
  #29 = Utf8               BMW car drive.
  #30 = NameAndType        #40:#41        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #31 = NameAndType        #42:#43        // toString:()Ljava/lang/String;
  #32 = Class              #44            // java/io/PrintStream
  #33 = NameAndType        #45:#46        // println:(Ljava/lang/String;)V
  #34 = Utf8               BMWCar
  #35 = Utf8               java/lang/Object
  #36 = Utf8               Car
  #37 = Utf8               java/lang/System
  #38 = Utf8               out
  #39 = Utf8               Ljava/io/PrintStream;
  #40 = Utf8               append
  #41 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #42 = Utf8               toString
  #43 = Utf8               ()Ljava/lang/String;
  #44 = Utf8               java/io/PrintStream
  #45 = Utf8               println
  #46 = Utf8               (Ljava/lang/String;)V
{
  public BMWCar();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: aload_0
         5: ldc           #2                  // String 宝马
         7: putfield      #3                  // Field name:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 4
        line 8: 10

  public void drive();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #5                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #6                  // Method java/lang/StringBuilder."":()V
        10: ldc           #7                  // String BMW car drive.
        12: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: aload_0
        16: getfield      #3                  // Field name:Ljava/lang/String;
        19: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        25: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        28: return
      LineNumberTable:
        line 12: 0
        line 13: 28
}
SourceFile: "BMWCar.java"

从这里也可以看到,常量池的常量数为46。常量池中主要存放2大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量主要指文本字符串、被声明为final的常量值。
符号引用主要包括以下3类:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池中的每个常量都是一个表,共有11种不同的表结构,它们有一个共同的特点,就是表开始的第一位都是一个u1类型的标志位(tag,取值为1到12,缺少标志为2的数据类型)。
tag表示的数据类型如下表所示:

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量,boolean、byte、char、short等类型都用int存放
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 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用

从以上表中可以看到,1-12除了类型2之外,共11种表数据类型,以下是一份常量池数据结构总表:

Java Class文件结构解析_第4张图片
常量数据结构表

接下来我们分析常量池里的第一个常量:

Java Class文件结构解析_第5张图片
第一个常量

这里可以看到,第一个常量的tag = 10,从前面的常量数据结构表中可以看到,10表示该常量为CONSTANT_Methodref_info,表示方法的符号引用,接下来有两个字节class_index = 12,指向常量池中索引值为12的常量CONSTANT_Class_info,再接下来2个字节name_and_type_index = 23,指向常量池中索引值为23的CONSTANT_NameAndType_info,与前面我们采用java命令查看class的信息是一致的。

  #1 = Methodref          #12.#23        // java/lang/Object."":()V
  #12 = Class              #35            // java/lang/Object
  #23 = NameAndType        #16:#17        // "":()V

这里也可以看到第1个常量是一个方法的符号引用,它指向了第12个常量和第23个常量,第12个常量表示类的符号引用,它指向了第35个常量,第23个常量又指向了第16、17个常量。
这样一个一个常量去分析,我们就可以计算出整个常量池在class文件中所占的字节空间,如下图所示:

Java Class文件结构解析_第6张图片

6.访问标志

常量池之后,是2个字节来表示访问标志,用于识别一些类或者接口层次的访问信息。

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可以设置
ACC_SUPER 0x0020 JDK1.0.2以后这个标志都为真
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或抽象类来说,此标志值为真,其他类值为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

针对BMWCar这个类来说,其访问标志应该是ACC_PUBLIC、ACC_SUPER这2个标志为真,所以其值为 0x0001 | 0x0020 = 0x0021。

Java Class文件结构解析_第7张图片
access_flag

7.类索引、父类索引

Java Class文件结构解析_第8张图片

访问标志后的2个字节000b = 11,指向常量池里的第11个常量,接下来的000c = 12,指向常量池里的第12个常量(该类的父类是Object)

8.接口索引

img_df68af6540d0f3e281077119101d848a.png

在本例子中,只有一个接口,0x000d十进制就是13,指向常量池中第13个常量,表示接口信息。

9.字段表

接口索引后面紧跟着的是字段表信息,字段表的入口前2个字节表示字段的个数,在本例子中只定义了个一个字段,所以其值为0x0001,后面紧跟着的是该字段的描述表。字段信息结构表如下:

类型 描述 备注
u2 access_flags 记录字段的访问标志
u2 name_index 常量池中的索引项,指定字段的名称
u2 descriptor_index 常量池中的索引项,指定字段的描述符
u2 attributes_count attributes包含的项目数
attribute_info attributes[attributes_count]

字段访问标志:

权限名称 描述
ACC_PUBLIC 0x0001 public
ACC_PRIVATE 0x0002 private
ACC_PROTECTED 0x0004 protected
ACC_STATIC 0x0008 static,静态
ACC_FINAL 0x0010 final
ACC_VOLATILE 0x0040 volatile,不可和ACC_FIANL一起使用
ACC_TRANSIENT 0x0080 在序列化中被忽略的字段
ACC_SYNTHETIC 0x1000 由编译器产生,不存在于源代码中
ACC_ENUM 0x4000 enum

紧随access_flags标志的是name_index和descriptor_index,他们都是对常量池的引用。name_index代表着字段的简单名称,descriptor_index代表着字段的描述符。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
描述符标识字符含义:

标识字符 含义
B 基本类型byte
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 特殊类型void
L 对象类型,如Ljava/lang/Object;
[ 数组类型,多个维度则有多个[

用描述符来描述方法时,按照先参数列表后返回值的顺序描述,参数列表按照参数的严格顺序放在一组“()”之内。
例如方法int getAge()的描述符为“()I”,方法void print(String msg)的描述符为“(Ljava/lang/String;)V”,方法int indexOf(int index, char[] arr)的描述符为“(I[C)I”。

再来看我们这个字节码:

Java Class文件结构解析_第9张图片
field

我们来具体分析下字节码:

  • 前两个字节0x0001表示字段计数值为1,即该类有1个字段;
  • 0x0002是第一个字段的访问标志,这里为private;
  • 0x000e是name_index,查找常量池可知其值为“name”;
  • 0x000f是descriptor_index,查找常量池可知其值为“Ljava/lang/String;”,至此通过字节码分析,我们可以反推出这个字段的定义为:private String name;
  • 0x0000表示attribute_count = 0,说明本字段没有额外的描述信息。但是如果该字段的声明为“private static final String name = "123"”,那就会存在一项名为ConstantValue的属性,其值指向常量“123”;

10.方法表

方法表的结构与字段表的结构是一样的。

类型 描述 备注
u2 access_flags 记录方法的访问标志
u2 name_index 常量池中的索引项,指定方法的名称
u2 descriptor_index 常量池中的索引项,指定方法的描述符
u2 attributes_count attributes包含的项目数
attribute_info attributes[attributes_count]

再来看看我们这个例子:

Java Class文件结构解析_第10张图片
method
  • 前2个字节0x0002表示有2个方法,一个是编译器添加的实例构造器方法,一个是源码中的drive()方法;
  • 接下来2个字节是访问标志,0x0001表示第一个方法是public;
  • name_index值为0x0010,查找常量表可得方法名为“”;
  • descriptor_index值为0x0011,对应常量值为“()V”,所以我们可以反推出该方法的定义为:public void (),这其实是编译器自动添加的一个实例构造器方法,我们的源码里并没有该方法;
  • 属性表计数器为0x0001,表示只有一个属性,接下来就是该方法的第一个属性表。第一个属性表对应的属性名称索引值为0x0012,对应常量值为“Code”,说明此属性是方法的字节码描述;
  • 行文至此,方法的定义可以通过access_flags、name_index、descriptor_index来表达清楚,但是方法里的代码去哪里了呢?前面刚提到该方法有一个名为“code”的属性,这个属性就存储了方法里的java代码编译后的字节码指令。

11.属性表

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

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类、方法表、字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
SourceFile 类文件 源文件名称
Synthetic 类、方法表、字段表 标识方法或字段为编译器自动生成的

不同的属性有不同的数据结构,在这里我们挑选2个最重要的属性来讲解一下:

11.1 Code属性

Java方法里的代码被编译处理后,变为字节码指令存储在方法表的Code属性里,但并不是所有的方法表里都有Code属性,例如接口或抽象类中的方法就可能没有该属性。

Code属性数据结构:

类型 名称 含义
u2 attribute_name_index 属性名称索引
u4 attribute_length 属性长度
u2 max_stack 操作数栈深度的最大值
u2 max_locals 局部变量表所需的存储空间
u4 code_length 字节码长度
u1 code[code_length] 存储字节码指令的一系列字节流
u2 exception_table_length 异常表长度
exception_info exception_table
u2 attributes_count
attribute_info attributes[attributes_count]

可以看到Code属性数据结构里还包含有其他属性,主要有LineNumberTable、LocalVariableTable。
LineNumberTable属性数据结构为:

LineNumberTable_attribute {
    u2 attribute_name_index;        //属性名称索引
    u4 attribute_length;            //属性长度
    u2 line_number_table_length;
    {   u2 start_pc;            //字节码行号
        u2 line_number;         //java源码行号
    } line_number_table[line_number_table_length];
}

我们接着前面方法表的字节码继续分析,前面分析到方法只有一个名为Code的属性值,参照Code属性的数据结构我们可以看到如下图所示:

Java Class文件结构解析_第11张图片
Code
11.2 ConstantValue属性

只有当一个字段被声明为static final时,并且该字段是基本数据类型或String类型时,编译器才会在字段的属性表集合中增加一个名为ConstantValue的属性,所以ConstantValue属性只会出现在字段表中,其数据结构为:

类型 名称 含义
u2 attribute_name_index 属性名称索引
u2 attribute_length 属性长度
u2 constantvalue_index 常量池常量的索引

12. 字段不同访问标志的初始化赋值差异

我们先来看个例子,里面定义了3种不同访问标志的字段:

public class Simple {

    public static final String a = "abc";
    
    public static String b = "abc";

    public String c = "abc";
    
}

这个类里面定义了3个不同的变量,都是赋值成“abc”,但是虚拟机对它们的赋值时机是不同的:

  1. 对于非静态(无static修饰)的字段,赋值会在实例构造方法()方法中进行。
  2. 对于static修饰的字段:如果有final修饰并且该字段是基本数据类型或String类型,则会在该字段对应的字段表field_info中增加一个名为ConstantValue的属性,赋值的时候使用该ConstantValue进行赋值;如果有final修饰,但该字段不是基本数据类型及String类型,则会在类构造方法
    ()中赋值;如果没有final修饰,则会在类构造方法()中赋值。
访问标志 数据类型 赋值策略
static final 基本数据类型或String ConstantValue
static final 除基本数据和String以外 ()方法
static 任意类型 ()方法
无static 任意类型 ()方法

采用javac Simple.java编译该类之后,同样采用javap -v Simple.class查看class文件的字节码信息,可以看到这3个字段的赋值方式:

Java Class文件结构解析_第12张图片

java类加载机制系列文章:

  • Java虚拟机类加载机制
  • Java Class文件结构解析
  • 类初始化造成的死锁
  • Java类加载器

你可能感兴趣的:(Java Class文件结构解析)