Java Class 探秘
Contents
前言
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 |
我们把如下文件编译成字节码
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.class 在 Emacs 中使用 hexl-mode 打开,如下所示。
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 表的结构
table CONSTANT_Class_info{
u1 tag = 7;
u2 name_index;
}
我们接着往下看第一个常量, 他的 tag 是 0a 转换成 10 进制是10,从上面的表中可以知道, tag 为 10 的是 CONSTANT_MethodRef_info ,它的结构是
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 得到结果如下
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 也就是没有实现接口。
在常量池中 0005 和 0006 分别是 HelloWorld 和 Object ,从字节码中看出 Object 是所有类的父类。
#5 = Class #23 // HelloWorld
#6 = Class #24 // java/lang/Object
字段表
字段表跟在接口索引计数器后面,类中的字段也可能有多个,所以也需要一个计数器,但是我们没有写任何字段,所以这个值依然是 0000 。
虽然我没有写任何字段,但还是要了解一下字段表的结构,如下所示
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 ,所以一共有三个方法,除了我们显示的写出来的 main 和 add 多出来的一个是构造方法。
方法表的结构如下
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 方法是用 public 和 static 修饰的,所以 access_flags 为 0009 。 接下来的两个字节为 000d 转换为 10 进制是 13,13 所对应的索引是 add 也就是方法名。再接下来是 000e 其对应的10进制是 14 也就是 int 也就是返回值类型。
#13 = Utf8 add
#14 = Utf8 (II)I
//...
#20 = NameAndType #13:#14 // add:(II)I
属性表
最后来看一下属性表,之前在字段表中和方法表中都有看到过这么一个属性 attribute_info attributes; 它没有一个固定的结构,只要满足如下结构就行
CONSTANT_Attribute_info{
u2 name_index;
u2 attribute_length length;
u1[] info;
}
JVM 中预定义了许多属性表,我们以 add 方法的 Code 属性表为例。我们接着字节码往下分析,得到 0001 和 0009
也就是 add 方法有一个属性表,属性表的索引为 9,9对应的就是 Code 属性表
#9 = Utf8 Code
Code 所对应的就是 add 方法的字节码指令,如下所示
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 文件还是比较复杂的,了解了这些我们更容易理解虚拟机是怎么运作的。有些问题就迎刃而解,例如:泛型被擦除了,在使用的时候会做一次强制类型转换等等。