一、class文件概述
class文件是指被编译器编译成的.class字节码文件,我们可任意打开一个class文件(使用Notepad的HexEditor插件打开http://down2.121down.com:8181/soft/hexeditor_dll.rar),内容如下(内容是16进制):
class文件字节码结构组织示意图:
我们可以将class的文件组织结构概括成以下面这个表格(其中u4表示4个无符号字节,u2表示2个无符号字节):
类型 | 名称 | 数量 |
---|---|---|
u4 | magic(魔数) | 1 |
u2 | minor_version(JDK次版本号) | 1 |
u2 | major_version(JDK主版本号) | 1 |
u2 | constant_pool_count(常量池数量) | 1 |
cp_info | constan_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 | attributes_count(属性数量) | 1 |
attribute_info | attributes(属性表) | attributes_count |
1.1 魔数
所有的由Java编译器编译而成的class文件的前4个字节都“0xCAFEBABE”。
它的作用在于:当JVM在尝试加载某个文件到内存中来的时候,会首先判断此class文件有没有JVM认为可以接受的“签名”,即JVM会首先读取文件的前4个字节,判断该4个字节是否是“0xCAFEBABE”,如果是,则JVM会认为可以将此文件当作class文件来加载并使用。
1.2 版本号
随着Java本身的发展,Java语言特性和JVM虚拟机也会有相应的更新和增强。目前我们能够用到的JDK版本如:1.5,1.6,1.7,还有现如今的1.8及更高的版本。发布新版本的目的在于:在原有的版本上增加新特性和相应的JVM虚拟机的优化。而随着主版本发布的次版本,则是修改相应主版本上出现的bug。我们平时只需要关注主版本就可以了。
主版本号和次版本号在class文件中各占两个字节,副版本号占用第5、6两个字节,而主版本号则占用第7,8两个字节。JDK1.0的主版本号为45,以后的每个新主版本都会在原先版本的基础上加1。若现在使用的是JDK1.8编译出来的class文件,则相应的主版本号应该是52,对应的7,8个字节的十六进制的值应该是 0x34。
一个 JVM实例只能支持特定范围内的主版本号 (Mi 至Mj) 和 0 至特定范围内 (0 至 m) 的副版本号。假设一个 Class 文件的格式版本号为 V, 仅当Mi.0 ≤ v ≤ Mj.m成立时,这个 Class 文件才可以被此 Java 虚拟机支持。不同版本的 Java 虚拟机实现支持的版本号也不同,高版本号的 Java 虚拟机实现可以支持低版本号的 Class 文件,反之则不成立。
JVM在加载class文件的时候,会读取出主版本号,然后比较这个class文件的主版本号和JVM本身的版本号,如果JVM本身的版本号 < class文件的版本号,JVM会认为加载不了这个class文件,会抛出我们经常见到的" java.lang.UnsupportedClassVersionError: Bad version number in .class file "Error 错误;反之,JVM会认为可以加载此class文件,继续加载此class文件。
1.3 常量池计数器
常量池是class文件非常重要的结构,它描述整个文件的字面量信息。常量池是由一组constant_pool结构体数组组成的,而数组的大小由常量池计数器指定。常量池计数器constant_pool_count的值=constant_pool数组中的成员值加1。
注意事项:
常量池计数器默认从1开始而不是从0开始:当constant_pool_count = 1时,常量池中的cp_info(常量池项)个数为0;当constant_pool_count为n时,常量池中的cp_info个数为n-1。
原因:
在指定class文件规范的时候,将索引#0项常量空出来是有特殊考虑的,这样当:某些数据在特定的情况下想表达“不引用任何一个常量池项”的意思时,就可以将其引用的常量的索引值设置为#0来表示。
1.3.1 常量池项范围
- 字面量
- 文本字符串
- 被声明成final的常量值
- 基本数据类型的值
- 其他
- 符号引用
- 类和结构的完全限定名
- 字段的名称和描述符
- 方法的名称和描述符
1.3.2 类索引
类索引,this_class的值必须是对constant_pool表中项目的一个有效索引值。constant_pool表在这个索引处的项必须为CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类或接口。
1.3.2 父类索引
父类索引,对于类来说,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类,只有它是唯一没有父类的类。
1.4 接口计数器
接口计数器,interfaces_count的值表示当前类或接口的【直接父接口数量】。
1.4.1 接口信息数据区
接口表,interfaces[]数组中的每个成员的值必须是一个对constant_pool表中项目的一个有效索引值, 它的长度为 interfaces_count。每个成员interfaces[i] 必须为CONSTANT_Class_info类型常量,其中 【0 ≤ i <interfaces_count】。在interfaces[]数组中,成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即interfaces[0]对应的是源代码中最左边的接口。
1.5 字段计数器
字段计数器,fields_count的值表示当前 Class 文件 fields[]数组的成员个数。 fields[]数组中每一项都是一个field_info结构的数据项,它用于表示该类或接口声明的【类字段】或者【实例字段】。
1.5.1 字段信息数据区
字段表,fields[]数组中的每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接口中某个字段的完整描述。 fields[]数组描述当前类或接口声明的所有字段,但不包括从父类或父接口继承的部分。
1.6 方法计数器
方法计数器, methods_count的值表示当前Class 文件 methods[]数组的成员个数。Methods[]数组中每一项都是一个 method_info 结构的数据项。
1.5.1 方法信息数据区
方法表,methods[] 数组中的每个成员都必须是一个 method_info 结构的数据项,用于表示当前类或接口中某个方法的完整描述。
如果某个method_info 结构的access_flags 项既没有设置 ACC_NATIVE 标志也没有设置ACC_ABSTRACT 标志,那么它所对应的方法体就应当可以被 Java 虚拟机直接从当前类加载,而不需要引用其它类。
method_info结构可以表示类和接口中定义的所有方法,包括【实例方法】、【类方法】、【实例初始化方法】和【类或接口初始化方法】。methods[]数组只描述【当前类或接口中声明的方法】,【不包括从父类或父接口继承的方法】。
二、class常量池理解
2.1 常量池在class文件什么位置?
2.2 常量池里面的项是怎么组织的?
每个要存储到常量池中的数据,需要对应一个或两个常量池项(cp_info)
2.3 常量池项(cp_info)的结构是什么?
每个cp_info都会记录着class文件中某种类型的字面量。JVM根据tag值确定cp_info表示的具体类型
JVM虚拟机规定了不同的tag值和不同类型的字面量对应关系如下:
tag值 | 表示的字面量 | 更细化的结构 |
---|---|---|
1 | 表示Utf8编码的字符串 | CONSTANT_Utf8_info |
3 | 表示int的数值常量 | CONSTANT_Integer_info |
4 | 表示float的数值常量 | CONSTANT_Float_info |
5 | 表示long的数值常量 | CONSTANT_Long_info |
6 | 表示double的数值常量 | CONSTANT_Double_info |
7 | 表示类或接口的完全限定名 | CONSTANT_Class_info |
8 | 表示String类型的常量对象 | CONSTANT_String_info |
9 | 表示类中的字段 | CONSTANT_Fieldref_info |
10 | 表示类中的方法 | CONSTANT_MethodRef_info |
总结:
- 一个常量池项:字面量型结构体
- 两个常量池项:引用型结构体,一个常量池项存储引用类型,引用类型的值使用UTF-8这种方式来存储。
2.4 int和float数据类型的常量在常量池中是怎样表示和存储的?
代码中所有用到 int 类型 10 的地方,会使用指向常量池的指针值#X定位到值为10的结构体 CONSTANT_Integer_info。而用到float类型的11f时,也会指向常量池的指针值#Y来定位到值为11f的结构体CONSTANT_Float_info。如下图所示:
2.5 long 和double数据类型的常量在常量池中是怎样表示和存储的?
long 类型和 double类型的数据类型占用8 个字节的空间,在常量池中,将long和double类型的常量分别使用CONSTANT_Long_info和Constant_Double_info表示,他们的结构如下所示:
long和double不加final修饰也会添加到常量池
CPU指令去执行命令的时候,需要操作一些数据,那么操作的数据大小,受限于32位系统或者64位系统。也就是说32位操作系统,CPU指令最长只能去操作4个字节长度的数据。64位操作系统,CPU指令最长只能去操作8个字节长度的数据。所以说为了保证任何操作系统都可以针对同一份class文件进行执行,我们把class中的数据以最大4个字节来存储。
2.6 String数据类型的常量在常量池中是怎样表示和存储的?
对于字符串而言,JVM会将字符串类型的字面量以UTF-8 编码格式存储到在class字节码文件中。其结构如下所示:
如上图所示的结构体,CONSTANT_String_info结构体中的string_index的值指向了CONSTANT_Utf8_info结构体,而字符串的utf-8编码数据就在这个结构体之中。如下图所示:
2.7 类文件中定义的类名和类中使用到的类在常量池中是怎样被组织和存储的?
总结:
- 对于某个类或接口而言,其自身、父类和继承或实现的接口的信息会被直接组装成CONSTANT_Class_info常量池项放置到常量池中;
- 类中或接口中使用到了其他的类,只有在类中实际使用到了该类时,该类的信息才会在常量池中有对应的CONSTANT_Class_info常量池项;
2.8 哪些字面量会进入常量池中?
- final类型的8种基本类型的值会进入常量池
- 非final类型的8种基本类型的值,只有float、double、long的值会进入常量池
- 常量池中包含字符串类型的字面量(双引号引起来的字符串的值)
- JVM里面还有一个常用的数字常量池,其范围是-128~127
三、class文件中的引用和特殊字符
3.1 符号引用
在class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。
特点:
- 符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。
- 符号引用指向了类的名称或者方法的名称或者字段的名称等,不是内存中的表示方式。
public class People{
private Language language;
}
比如 org.simple.People类 引用了 org.simple.Language类 ,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号 org.simple.Language (假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。
3.2 直接引用
直接引用可以是:
- 直接指向目标的指针
- 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
- 一个能间接定位到目标的句柄
3.3 引用替换的时机
符号引用替换为直接引用的操作发生在【类加载过程】(加载 -> 连接(验证、准备、解析) -> 初始化)中的解析阶段,会将【符号引用】转换(替换)为对应的【直接引用】,放入运行时常量池中。
3.3 特殊字符串字面量
特殊字符串包括三种:类的全限定名、字段和方法的描述符、特殊方法的方法名
3.3.1 类的全限定名
Object类,在源文件中的全限定名是 java.lang.Object 。而class文件中的全限定名是将点号替换成“/” 。 也就是 java/lang/Object 。源文件中一个类的名字, 在class文件中是用全限定名表述的。
3.3.2 描述符
对于字段的数据类型,其描述符主要有以下几种
数据类型 | 描述符 |
---|---|
bye | B |
char | C |
double | D |
float | F |
int | I |
long | J |
shirt | S |
boolean | Z |
特殊类型void | V |
对象类型 | “L”+全限定名+";"。如Ljava/lang/Stirng;表示String类型。 |
数组类型 | 若干个 “[” + 数组中元素类型的对应字符串,如一维数组 int[] 的描述符为 [I ,二维数组 String[][] 的描述符为 [[java/lang/String; |
3.3.2.1 字段描述符
字段的描述符就是字段的类型所对应的字符或字符串。
- int i 中, 字段i的描述符就是 I
- Object o中, 字段o的描述符就是 Ljava/lang/Object;
- double[][] d中, 字段d的描述符就是 [[D
3.3.2.2 方法描述符
方法描述符比较复杂,包括所有参数的类型列表和方法返回值。它的格式是这样的:(参数1类型 参数2类型 参数3类型 …)返回值类型
不管是参数的类型还是返回值的类型,都是使用对应字符和对应字符串来表示的,并且参数列表使用小括号括起来,并且各个参数之间没有空格、参数列表和返回值之间也没有空格
方法描述符 | 方法声明 |
---|---|
()I | int getSize() |
()Ljava/lang/String; | String toString() |
([Ljava/lang/String;)V | void main(String[] args) |
()V | void wait() |
(JI)V | void wait(long timeout, int nanos) |
(ZILjava/lang/String;II)Z | boolean regionMatches(boolean ignoreCase, int toOffset, Stringother, int ooffset, int len) |
([BII)I | int read(byte[] b, int off, int len ) |
()[[Ljava/lang/Object; | Object[][] getObjectArray() |
3.3.3 特殊的方法名
首先要明确一下, 这里的特殊方法是指的类的构造方法和类型初始化方法。
构造方法就不用多说了, 至于类型的初始化方法, 对应到源码中就是静态初始化块。 也就是说, 静态初始化块, 在class文件中是以一个方法表述的, 这个方法同样有方法描述符和方法名,具体如下:
- 类的构造方法的方法名使用字符串" "表示
- 静态初始化方法的方法名使用字符串" "表示。
- 除了这两种特殊的方法外,其他普通方法的方法名,和源文件中的方法名相同。
总结
1.方法和字段的描述符中。不包括字段名和方法名,字段描述符中只包括字段类型,方法描述符中参数列表和返回值的类型。
2.无论method()是静态方法还是实例方法,它的方法描述符都是相同的。实例方法还需要额外传递参数this,它是由JVM在调用实例方法所使用的指令中实现的隐式传递。