JVM:Class文件的结构
不看不知道,一看吓一跳,一个class文件里面居然包含了这么多东西。让我们揭开 class 文件的面纱,来看看 class 文件的结构吧。
一、简介
Class 文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,使得整个Class文件中存储的内容几乎全都是程序运行的必要数据,没有空隙存在。
当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
Class文件采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
- 无符号数属于基本的数据类型,以u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表的习惯地以
"_info"结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。
| 类型 | 名称 | 数量 |
|---|---|---|
| u4 | magic | 1 |
| u2 | minor_version | 1 |
| u2 | major_version | 1 |
| u2 | constant_pool_count | 1 |
| cp_info | constant_pool | constant_pool_count -1 |
| u2 | access_flags | 1 |
| u2 | this_class | 1 |
| u2 | super_class | 1 |
| u2 | interfaces_count | 1 |
| u2 | interfaces | interfaces_count |
| u2 | fields_count | 1 |
| field_info | fields | fields_count |
| u2 | methods_count | 1 |
| method_info | methods | methods_count |
| u2 | attribute_count | 1 |
| attribute_info | attributes | attributes_count |
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容器计数器加若干个连续的数据项的形式。在Class文件中,无论是顺序还是数量都是被严格限制的,哪个字节代表什么意义,长度是多少,先后顺序如何,都不允许改变。
二、魔数
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否是一个能被虚拟机接受的Class文件。Class文件的魔数值为:0xCAFEBABY(我一直这么记:咖啡宝贝)。
三、Class 文件的版本
紧接着魔数的4个字节存储的是Class文件的版本号:前两个字节表示次版本号,后两个字节是主版本号。
例如,JDK1.1能支持版本号为45.0~45.65535的Class文件,而JDK1.2则能支持45.0~46.65535的Class文件。高版本的JDK可以向下兼容以前版本的Class文件,但不能运行以后的Class文件,即使文件格式并未发生任何变化,虚拟机也会拒绝执行超过其版本号的Class文件。
四、常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
(一)常量池大小
由于常量池的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。值得注意的是,常量池的容量计数是从1而不是0开始的。
假如一个Class文件的表示常量池容量的字段值为0x0016,即十进制的22,这就代表常量池有21项常量,索引值范围为1~21。这里空出了索引为0的情况,这样做的目的就在于满足后面某些指向常量池的索引值的数据在待定的情况下需要表达“不引用任何常量池项目”的含义。
Class文件中只有常量池的容量计数是从1开始的,对于其他集合类型,都是从0开始的。
(二)常量池存放类型
常量池主要存放两大类常量:字面量和符号引用。
字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等(这里想到一点类加载机制的知识,final修饰的常量在编译时就被加载到方法区中,而不是等到初始化之后才给这个常量赋值)。
符号引用则属于编译原理方面的概念,包括下面三大常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
Java代码在进行javac编译的时候,并不像C++那样有连接这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。
也就是说在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法被虚拟机直接使用。当虚拟机运行的时候,需要从常量池中获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
(三)常量池具体类型
常量池中每一项常量都是一个表,在JDK1.7中,一共有14中结构各不相同的表结构。
这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量属于那种常量类型。
下面是这14中常量的具体含义(其实看英文都大致可以看得出来):
| 常量池中数据项类型 | 类型标志 | 类型描述 |
|---|---|---|
| CONSTANT_Utf8_info | 1 | UTF-8编码的Unicode字符串 |
| CONSTANT_Integer_info | 3 | int类型字面值 |
| CONSTANT_Float_info | 4 | float类型字面值 |
| CONSTANT_Long_info | 5 | long类型字面值 |
| CONSTANT_Double_info | 6 | double类型字面值 |
| CONSTANT_Class_info | 7 | 对一个类或接口的符号引用 |
| CONSTANT_String_info | 8 | String类型字面值 |
| 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 | 表示一个动态方法调用点 |
我们来看一个例子:

我们可以看到前面四个字节(u4)就是魔数 CAFEBABE