前言:
class文件是一个16进制的文件,可以通过Hex Editor工具打开,打开后如下:
注:提供两个文档帮助我们阅读class文件
概述
我们先对这个class文件有个总体的了解
魔数
所有的由Java编译器编译而成的class文件的前4个字节都是“0xCAFEBABE”。(简称cafebabe-咖啡宝贝)
它的作用在于:当JVM在尝试加载某个文件到内存中来的时候,会首先判断此class文件有没有JVM认为
可以接受的“签名”,即JVM会首先读取文件的前4个字节,判断该4个字节是否是“0xCAFEBABE”,如
果是,则JVM会认为可以将此文件当作class文件来加载并使用。
版本号
主版本号和次版本号在class文件中各占两个字节,副版本号占用第5、6两个字节,而主版本号则占用
第7,8两个字节。
JDK版本号信息对照表:
注:有时候我们在运行程序时会抛出这个Error 错
误:“java.lang.UnsupportedClassVersionError: Bad version number in .class
file”。上面已经揭示了出现这个问题的原因,就是在于当前尝试加载class文件的JVM虚拟
机的版本 低于class文件的版本。
可以用当前虚拟机重新编译或者将当前虚拟机更新到和class文件一样的版本。
常量池计数器
它占两个字节,表示常量池中的个数。
常量池计数器默认从1开始而不是从0开始,把下标为0的空余出来
常量池数据区
常量池是class文件中非常重要的结构,它描述着整个class文件的字面量信息。常量池是由一组
constant_pool结构体数组组成的,而数组的大小则由常量池计数器指定。它的个数是常量池计数器的值-1。
那么常量池项中都存哪些数据呢?
那么常量池项 (cp_info)具体的结构是啥呢?
JVM虚拟机规定了不同的tag值和不同类型的字面量对应关系如下:
int和float存储的结构如下:
long和double存储的结构如下:
需要注意的是long和double占8个字节,但是存储的时候是由两个4字节分开来的。这是受限于操作系统是32位导致的,32位就是4个字节,一次只能操作4个字节。
String类型存储的结构如下:
如上图所示的结构体,CONSTANT_String_info结构体中的string_index的值指向了
CONSTANT_Utf8_info结构体,而字符串的utf-8编码数据就在这个结构体之中,即tring_index的值hi某个CONSTANT_Utf8_info结构体在常量池中的索引。如下图所示:
其中length表示这个utf-8编码的字节数组的长度,即有多少个字节。
byte[length]表示使用了utf-8编码后的具体字节数组。类文件中定义的类名和类中使用到的类的结构:
JVM会将某个Java 类中所有使用到了的类的完全限定名 以二进制形式的完全限定名 封装成
CONSTANT_Class_info结构体中,然后将其放置到常量池里。CONSTANT_Class_info 的tag值为 7
其中 name_index的值是某个CONSTANT_Utf8_info结构体在常量池中的索引,对应的CONSTANT_Utf8_info结构体中存储了对应的二进制形式的完全限定名称的字符串,即类的全路径,但是会把".“换成”/",比如:
哪些字面量会进入常量池中?
- final类型的8种基本类型的值会进入常量池。
- 非final类型(包括static的)的8种基本类型的值,只有double、float、long的值会进入常量池。
- 常量池中包含的字符串类型字面量(双引号引起来的字符串值)。
访问标志
访问标志,access_flags 是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。
类索引
类索引,this_class的值必须是对constant_pool表中项目的一个有效索引值。constant_pool表
在这个索引处的项必须为CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类或接
口。
父类索引
父类索引,对于类来说,super_class 的值必须为 0 或者是对constant_pool 表中项目的一个有
效索引值。
如果它的值不为 0,那 constant_pool 表在这个索引处的项必须为CONSTANT_Class_info 类型常
量,表示这个 Class 文件所定义的类的直接父类。当前类的直接父类,以及它所有间接父类的
access_flag 中都不能带有ACC_FINAL 标记。对于接口来说,它的Class文件的super_class项的
值必须是对constant_pool表中项目的一个有效索引值。constant_pool表在这个索引处的项必须为
代表 java.lang.Object 的 CONSTANT_Class_info 类型常量 。
如果 Class 文件的 super_class的值为 0,那这个Class文件只可能是定义的是
java.lang.Object类,只有它是唯一没有父类的类。
接口计数器
接口计数器,interfaces_count的值表示当前类或接口的【直接父接口数量】。
接口信息数据区
接口表,interfaces[]数组中的每个成员的值必须是一个对constant_pool表中项目的一个有效索引
值, 它的长度为 interfaces_count。每个成员interfaces[i] 必须为
CONSTANT_Class_info类型常量,其中 【0 ≤ i <interfaces_count】。在interfaces[]数组
中,成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即interfaces[0]对
应的是源代码中最左边的接口。
字段计数器
字段计数器,fields_count的值表示当前 Class 文件 fields[]数组的成员个数。 fields[]数组
中每一项都是一个field_info结构的数据项,它用于表示该类或接口声明的【类字段】或者【实例字
段】。
字段信息数据区
字段表,fields[]数组中的每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接
口中某个字段的完整描述。 fields[]数组描述当前类或接口声明的所有字段,但不包括从父类或父接
口继承的部分。
方法计数器
方法计数器, methods_count的值表示当前Class 文件 methods[]数组的成员个数。Methods[]
数组中每一项都是一个 method_info 结构的数据项。
方法信息数据区
方法表,methods[] 数组中的每个成员都必须是一个 method_info 结构的数据项,用于表示当前类
或接口中某个方法的完整描述。
如果某个method_info 结构的access_flags 项既没有设置 ACC_NATIVE 标志也没有设置
ACC_ABSTRACT 标志,那么它所对应的方法体就应当可以被 Java 虚拟机直接从当前类加载,而不需
要引用其它类。
method_info结构可以表示类和接口中定义的所有方法,包括【实例方法】、【类方法】、【实例初始
化方法】和【类或接口初始化方法】。
methods[]数组只描述【当前类或接口中声明的方法】,【不包括从父类或父接口继承的方法】。
属性计数器
属性计数器,attributes_count的值表示当前 Class 文件attributes表的成员个数。
attributes表中每一项都是一个attribute_info 结构的数据项。
属性信息数据区
属性表,attributes 表的每个项的值必须是attribute_info结构。
以上就是class文件中的信息。
特殊字符串字面量
特殊字符串包括三种: 类的全限定名, 字段和方法的描述符, 特殊方法的方法名
- 类的全限定名
- 描述符
1.各类型的描述符
基本数据类型(byte、char、double、float、int、long、short、boolean):除 long 和
boolean,其他基本数据类型的描述符用对应单词的大写首字母表示。long 用 J 表示,
boolean 用 Z 表示。
void:描述符是 V。
对象类型:描述符用字符 L 加上对象的全限定名表示,如 String 类型的描述符为Ljava/lang/String 。
数组类型:每增加一个维度则在对应的字段描述符前增加一个 [ ,如一维数组 int[] 的描述
符为 [I ,二维数组 String[][] 的描述符为 [[Ljava/lang/String 。
2.字段描述符
int i 中, 字段i的描述符就是 I
Object o中, 字段o的描述符就是 Ljava/lang/Object;
double[][] d中, 字段d的描述符就是 [[D
3.方法描述符
方法的描述符比较复杂, 包括所有参数的类型列表和方法返回值。 它的格式是这样的:
(参数1类型 参数2类型 参数3类型 …)返回值类型 - 特殊方法的方法名
首先要明确一下, 这里的特殊方法是指的类的构造方法和类型初始化方法。
构造方法就不用多说了, 至于类型的初始化方法, 对应到源码中就是静态初始化块。 也就是说,
静态初始化块, 在class文件中是以一个方法表述的, 这个方法同样有方法描述符和方法名,具体如下:
类的构造方法的方法名使用字符串 表示
静态初始化方法的方法名使用字符串 表示。
除了这两种特殊的方法外, 其他普通方法的方法名, 和源文件中的方法名相同。