前言

Java 开发都知道 .java 文件编译后得到的是 .class 文件,这个文件是 Java 虚拟机能够识别加载的文件。有了这个字节码文件 Java 实现一次编写到处运行才成为可能。

然而我们平时都是写 .java 文件,很少接触 .class 文件,今天就来看看 .class 文件中有什么秘密,它长什么样子。

class 内部数据结构

class 文件中有两种数据结构,分别是无符号数和表

无符号数

无符号数有 u1、u2、u4、u8,分别表示1个字节、2个字节、4个字节和8个字节。

表由多个无符号数或者其他表作为数据项构成的符合数据类型。 class 中所有的表都是以 info 结尾。

class 文件结构

我们把 Java 文件编译成字节码之后,会按照下面所示的规则排列好 它们的数据类型如下所示

字段 名称 数据类型 数量
magic number 魔术数 u4 1
major version 主版本号 u2 1
minor version 副版本号 u2 1
constant_pool_conut 常量池大小 u2 1
constant_pool 常量池 cp_info constant_pool_conut-1
access_flag 访问标志 u2 1
this_class 当前类索引 u2 1
super_class 父类索引 u2 1
interfaces_count 接口索引集合大小 u2 1
interfaces 接口索引集合 u2 interfaces_count
fields_count 字段索引集合大小 u2 1
fields 字段索引集合 field_info fields_count
methods_count 方法索引集合大小 u2 1
methods 方法索引集合 method_info methods_count
attributes_count 属性索引集合大小 u2 1
attributes 属性索引集合 attribute_info attributes_count

我们把如下文件编译成字节码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class HelloWorld
{
    public static void main(String[] args)
    {
        System.out.println(add(1,2));
    }

    public static int add(int a,int b){
        return a+b;
    }
}

把编译好的 HelloWorld.classEmacs 中使用 hexl-mode 打开,如下所示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cafe babe 0000 0034 001f 0a00 0600 1109
0012 0013 0a00 0500 140a 0015 0016 0700
1707 0018 0100 063c 696e 6974 3e01 0003
2829 5601 0004 436f 6465 0100 0f4c 696e
654e 756d 6265 7254 6162 6c65 0100 046d
6169 6e01 0016 285b 4c6a 6176 612f 6c61
6e67 2f53 7472 696e 673b 2956 0100 0361
6464 0100 0528 4949 2949 0100 0a53 6f75
7263 6546 696c 6501 000f 4865 6c6c 6f57
6f72 6c64 2e6a 6176 610c 0007 0008 0700
190c 001a 001b 0c00 0d00 0e07 001c 0c00
1d00 1e01 000a 4865 6c6c 6f57 6f72 6c64
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 7401 0010 6a61 7661 2f6c 616e 672f
5379 7374 656d 0100 036f 7574 0100 154c
6a61 7661 2f69 6f2f 5072 696e 7453 7472
6561 6d3b 0100 136a 6176 612f 696f 2f50
7269 6e74 5374 7265 616d 0100 0770 7269
6e74 6c6e 0100 0428 4929 5600 2000 0500
0600 0000 0000 0300 0000 0700 0800 0100
0900 0000 1d00 0100 0100 0000 052a b700
01b1 0000 0001 000a 0000 0006 0001 0000
0001 0009 000b 000c 0001 0009 0000 0028
0003 0001 0000 000c b200 0204 05b8 0003
b600 04b1 0000 0001 000a 0000 000a 0002
0000 0005 000b 0006 0009 000d 000e 0001
0009 0000 001c 0002 0002 0000 0004 1a1b
60ac 0000 0001 000a 0000 0006 0001 0000
0009 0001 000f 0000 0002 0010

魔术数(magic number)

Java 中的前4个字节是 cafe babe ,也就是魔术数,表示的是可以被 Java 虚拟机执行的。如果不是以这个开头虚拟机就不会加载这个文件。

版本号

继魔术数之后是 class 文件的版本号,为 00 00 00 34 分为次版本号(minor version)和主版本号(major version)。也就是前面两个字节是次版本号,后面两个自己是主版本号。

所以这个文件的版本号转换为10进制后为 52.0 ,也就是 JDK1.8

常量池

版本号之后就是常量池,常量池相当于 class 的资源仓库。

常量池的大小通过常量池计数器来决定,常量池计数器等于常量池的大小+1,常量池中下标为 0 被虚拟机用作其他用途。

我们看到版本号后面的两个字节为 001f 转换为10进制为 31 ,所以常量池的大小为 30

常量池后面的就是各种表,保存了类的相关信息。例如:类名,父类名,方法名,参数名,参数类型等。常量池中有 14 种不同的表,如下所示

表名 标识位 描述
CONSTANT_utf8_info 1 UTF-8编码字符串表
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 字段或方法的名称和类型表
CONSTANT_MethodHandle_info 15 方法句柄表
CONSTANT_MethodType_info 16 方法类型表
CONSTANT_invokeDynamic_info 18 动态方法调用表

常量池中有这么多的表,所以在每个表中都有一个 u1 类型的 tag 用来标识是哪个表。

我们来看下 CONSTANT_Class_info 表的结构

1
2
3
4
table CONSTANT_Class_info{
    u1 tag = 7;
    u2 name_index;
}

我们接着往下看第一个常量, 他的 tag0a 转换成 10 进制是10,从上面的表中可以知道, tag 为 10 的是 CONSTANT_MethodRef_info ,它的结构是

1
2
3
4
5
CONSTANT_Methodref_info{
    u1 tag=10;
    u2 class_index;
    u2 name_type_index;
}

其中 class_index 指的是方法所属的类,占2个字节,也就是接来下的 0006 转换成10进制等于6,在常量池中的索引为6。

name_type_index 指的是方法的名称和类型,也是占两个字节,也就是 0011 ,转换成10进制等于 17,在常量池索引为 17 的地方。

单纯从16进制的 class 文件中很难看出来是什么,我们通过 javap -v HelloWorld.class 得到结果如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Constant pool:
   #1 = Methodref          #6.#17         // java/lang/Object."<init>":()V
   // ...
   #6 = Class              #24            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   // ...
  #17 = NameAndType        #7:#8          // "<init>":()V
  // ...
  #24 = Utf8               java/lang/Object

这样就非常清楚能看出来常量池中第一个是索引是 Object 的构造方法。

访问标志

字节码中的访问标识有如下几种

访问标志 描述
ACC_PUBLIC 0x0001 public 类型
ACC_FINAL 0x0010 final 类型
ACC_SUPER 0x0020 是否允许使用invokespecial 字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标识默认为True
ACC_INTERFACE 0x0200 接口类型
ACC_ABSTRACT 0x0400 标识这是一个抽象类或是接口类型
ACC_ANNOTATION 0x2000 注解类型
ACC_ENUM 0x4000 枚举类型

字节码的访问标识在常量池之后,占两个字节。如下所示

类索引、父类索引、接口索引计数器

访问标识之后就是类索引、父类索引和接口索引计数器。由于 Java 是单继承,可以实现多个接口所以接口要有一个计数器来确定有多少个接口。

可以看到在访问标识之后类索引就是 0005 ,父类索引是 0006 ,接口索引计数器是 0000 也就是没有实现接口。

在常量池中 00050006 分别是 HelloWorldObject ,从字节码中看出 Object 是所有类的父类。

1
2
#5 = Class              #23            // HelloWorld
#6 = Class              #24            // java/lang/Object

字段表

字段表跟在接口索引计数器后面,类中的字段也可能有多个,所以也需要一个计数器,但是我们没有写任何字段,所以这个值依然是 0000

虽然我没有写任何字段,但还是要了解一下字段表的结构,如下所示

1
2
3
4
5
6
7
CONSTANT_Fieldref_info{
    u2 access_flags;// 字段访问标识
    u2 name_index;// 变量名
    u2 descriptor_index;// 变量的类型
    u2 attributes_count; // 属性计数器
    attributes_info;
}

字段的访问标识和类的有点不一样,如下所示

字段访问标识 描述
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_TRANSIENT 0x0080 transient
ACC_ENUM 0x4000 enum

方法表

字段表接着往下走就是方法表,方法表和字段表一样也会有多个的情况,所以也有一个计数器。 从图中看出方法计数器的值为 0003 ,所以一共有三个方法,除了我们显示的写出来的 mainadd 多出来的一个是构造方法。

方法表的结构如下

1
2
3
4
5
6
7
CONSTANT_Methodref_info{
    u2 access_flags; // 方法访问标识
    u2 name_index; // 方法名
    u2 descriptor_index; // 返回值
    u2 attributes_count; // 方法属性计数器
    attribute_info attributes;
}

在方法表中也有 access_flags ,它和字段的也不一样,如下所示

访问标识 描述
ACC_PUBLIC 0x0001 public
ACC_PRIVATE 0x0002 private
ACC_PROTECTED 0x0004 protected
ACC_STATIC 0x0008 static
ACC_FINAL 0x0010 final
ACC_SYNCHRONIZED 0x0020 synchronized
ACC_VARARGS 0x0080 方法是否有形参
ACC_NATIVE 0x0100 native
ACC_ABSTRACT 0x0400 abstract

我们以 add 方法为例,如下所示 add 方法是用 publicstatic 修饰的,所以 access_flags0009 。 接下来的两个字节为 000d 转换为 10 进制是 13,13 所对应的索引是 add 也就是方法名。再接下来是 000e 其对应的10进制是 14 也就是 int 也就是返回值类型。

1
2
3
4
#13 = Utf8               add
#14 = Utf8               (II)I
//...
#20 = NameAndType        #13:#14        // add:(II)I

属性表

最后来看一下属性表,之前在字段表中和方法表中都有看到过这么一个属性 attribute_info attributes; 它没有一个固定的结构,只要满足如下结构就行

1
2
3
4
5
CONSTANT_Attribute_info{
    u2 name_index;
    u2 attribute_length length;
    u1[] info;
}

JVM 中预定义了许多属性表,我们以 add 方法的 Code 属性表为例。我们接着字节码往下分析,得到 00010009 也就是 add 方法有一个属性表,属性表的索引为 9,9对应的就是 Code 属性表

1
#9 = Utf8               Code

Code 所对应的就是 add 方法的字节码指令,如下所示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static int add(int, int);
descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: ireturn
      LineNumberTable:
        line 9: 0

总结

从上面看到 .class 文件还是比较复杂的,了解了这些我们更容易理解虚拟机是怎么运作的。有些问题就迎刃而解,例如:泛型被擦除了,在使用的时候会做一次强制类型转换等等。

参考