Java Class 探秘
文章目录
前言
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 |
我们把如下文件编译成字节码
|
|
把编译好的 HelloWorld.class 在 Emacs 中使用 hexl-mode 打开,如下所示。
|
|
魔术数(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 表的结构
|
|
我们接着往下看第一个常量, 他的 tag 是 0a 转换成 10 进制是10,从上面的表中可以知道, tag 为 10 的是 CONSTANT_MethodRef_info ,它的结构是
|
|
其中 class_index 指的是方法所属的类,占2个字节,也就是接来下的 0006 转换成10进制等于6,在常量池中的索引为6。
name_type_index 指的是方法的名称和类型,也是占两个字节,也就是 0011 ,转换成10进制等于 17,在常量池索引为 17 的地方。
单纯从16进制的 class 文件中很难看出来是什么,我们通过 javap -v HelloWorld.class 得到结果如下
|
|
这样就非常清楚能看出来常量池中第一个是索引是 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 是所有类的父类。
|
|
字段表
字段表跟在接口索引计数器后面,类中的字段也可能有多个,所以也需要一个计数器,但是我们没有写任何字段,所以这个值依然是 0000 。
虽然我没有写任何字段,但还是要了解一下字段表的结构,如下所示
|
|
字段的访问标识和类的有点不一样,如下所示
| 字段访问标识 | 值 | 描述 |
|---|---|---|
| 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 多出来的一个是构造方法。
方法表的结构如下
|
|
在方法表中也有 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 也就是返回值类型。
|
|
属性表
最后来看一下属性表,之前在字段表中和方法表中都有看到过这么一个属性 attribute_info attributes; 它没有一个固定的结构,只要满足如下结构就行
|
|
JVM 中预定义了许多属性表,我们以 add 方法的 Code 属性表为例。我们接着字节码往下分析,得到 0001 和 0009
也就是 add 方法有一个属性表,属性表的索引为 9,9对应的就是 Code 属性表
|
|
Code 所对应的就是 add 方法的字节码指令,如下所示
|
|
总结
从上面看到 .class 文件还是比较复杂的,了解了这些我们更容易理解虚拟机是怎么运作的。有些问题就迎刃而解,例如:泛型被擦除了,在使用的时候会做一次强制类型转换等等。