- class文件与虚拟机
- .class文件内容
- 常量池解析(重点掌握)
1.Class字节码文件由来
-
Java能够实现“一次编译,到处运行”,其中class文件要占大部分功劳。
- 为了让Java语言具有良好的跨平台能力,Java提供了一种可以在所有平台上都能使用的一种中间代码-字节码类文件(.class文件)
- 有了字节码,无论哪种平台(Mac,Windows,Linux等),只要安装了虚拟机都可以直接运行字节码
-
有了字节码,也解除了Java虚拟机和Java语言之间的耦合
- Java虚拟机也支持很多除Java语言以外的其他语言,如Groovy,JRuby,Jython,Scala等,因为这些语言经过编译之后也可以生成能够被JVM解析并执行的字节码文件。
-
虚拟机不关系字节码是由那种语言编译而来的。
2.class文件组成
class文件里只有两种数据结构:无符号数和表
- 无符号数:属于基本的数据类型
- 以u1,u2,u4,u8。来分别代表1个字节,2个字节,4个字节和8个字节的无符号数,
- 无符号数可以用来描述数字,索引引用,数量值或者字符串
- 表:
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型。
- class文件中所有的表都以"_info"结尾,整个class文件本质上就是一张表。
表和无符号数之间的关系:
2.表和无符号数之间的关系.png伪代码如下:
// 无符号数
u1 = byte[1];
u2 = byte[2];
u4 = byte[4];
u8 = byte[8];
// 表
class_table {
// 表中可以引用各种无符号数,
u1 tag;
u2 index2;
...
// 表中也可以引用其它表
method_table mt;
...
}
3.class文件结构
-
class文件中只存在无符号数和表两种数据结构。
- 这些无符号数和表组成了class中的各个结构;
- 这些结构按照预先规定好的顺序紧密的从前向后排列,相邻的项之间没有任何间隙。
-
class文件结构
魔数 | 版本号 | 常量池 | 访问标志 | 类/父类/接口 | 字段描述集合 | 方法描述集合 | 属性描述集合 |
---|
当JVM加载某个class文件时,jvm就是根据上图中的结构去解析class文件,加载class文件到内存中,并在内存中分配相应的空间。具体某一种结构需要占用多大空间,如下图:
字段 | 名称 | 数据类型 | 数量 |
---|---|---|---|
magic number | 魔数 | u4 | 1 |
major version | 主版本号 | u2 | 1 |
minor version | 副版本号 | u2 | 1 |
constant_pool_count | 常量池大小 | u2 | 1 |
constant_pool | 常量池 | cp_info | constant_pool_count-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结构关系
- class文件中的无符号和表相当于人类身体中的H,O,C,N等元素
- 而class结构图中的各项结果相当于人类身体的各个器官
- 并且这些器官的组织顺序是有严格顺序要求的
实例分析
import java.io.Serializable;
public class Test implements Serializable, Cloneable {
private int num = 1;
public int add(int i) {
int j = 10;
num = num + i;
return num;
}
}
- 通过javac编译,生成Test.class字节码,然后使用16进制编辑器打开class文件,查看内容
- 下图中都是一些16进制数字,每两个字符代表一个字节
- 其Test.class文件对应的字节码文件:javac -> javap -v Test.class
魔数 magic number
CA FE BA BE
- class文件开头的四个字节时class文件的魔数,它是一个固定的值--0XCAFEBABE
- 魔数是class文件的标志,是判断一个文件是不是class格式文件的标准,如果开头四个字节不是0XCAFEBABE,就说明不是class文件,不能被JVM识别或加载
版本号
00 00 00 34
- 跟在魔数后面的四个字节代表当前class文件的版本号。
- 前两个字节0000代表次版本号(minor_version),后两个字节0034是主版本号(major_version),对应十进制值为52.
- 也就是说当前class文件的主版本号为52,次版本号为0。也就是jdk1.8.0
常量池
- 紧跟在版本号之后的是一个叫做常量池的表(cp_info).
- 在常量池中保存了类的各种相关信息,比如类的名称,父类的名称,类中的方法名,参数名称,参数类型等,这些信息都是以各种表的形式保存在常量池中的
常量池中的每一项都是一个表,其项目类型共有14种,如下表:
表名 | 标识位(Tag) | 描述 |
---|---|---|
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值,tag值是表的标识,jvm解析class文件时,通过这个值来判断当前数据数据结果是哪一种表。
以CONSTANT_Class_info和CONSTANT_utf8_info两张表距离说明:
CONSTANT_Class_info表具体结果如下:
table CONSTANT_Class_info {
u1 tag = 7;
u2 name_index;
}
- 解析
- tag:占用一个字节大小。不如值位7,说明是CONSTANT_Class_info类型表
- name_index:是一个索引值,可以将它理解为一个指针,指向常量池种索引为name_index的常量表。比如name_index=2,则它指向常量池种第2个常量
CONSTANT_utf8_info表具体结果如下:
table CONSTANT_utf8_info {
u1 tag;
u2 length;
u1[] bytes;
}
- 解析
- tag:值为1,表示是CONSTANT_utf8_info类型表
- length:length表示u1[]的长度,比如length=5,则表示接下来的数据是5个连续的u1类型数据
- bytes:u1类型数组,长度为length的值
- 在java代码中声明的String字符串最终在class文件中的存储格式就是CONSTANT_utf8_info。因此一个字符串最大长度就是u2所能代表的最大值65535(2的16次方),但是需要使用2个字节来保存null值,因此一个字符串的最大长度为65535-2=65534.
在常量池内部的表中也有相互之间的引用。用一张图来理解CONSTANT_Class_info和CONSTANT_utf8_info表格之间的关系,如图:
4.常量池中表之间的关系.png接着往下解析,常量池元素个数
- 因为开发者平时定义的java类各式各样,类中的方法与参数也不尽相同。所以常量池的元素数量也就无法固定。
- 因此class文件在常量池的前面使用2个字节的容器计数器,用来代表当前类中常量池的大小。
- 如下图,红色框中的0017转换为十进制为23,也就是说常量计数器的值为23.其中下标为0的常量被jvm留作其他用途,因此Test.class中实际常量池大小为这个计数器的值减1,为22个
第一个常量,如下图
6.常量池第一个常量.png- 0A转化为十进制为10,通过查看常量池14种类型表格,tag=10的表类型为CONSTANT_Methodref_info,因此常量池种的第一个常量类型为方法引用表,结果如下:
CONSTANT_Methodref_info {
u1 tag = 10;
u2 class_index; 指向此方法的所属类
u2 name_type_index; 指向此方法的名称和类型
}
- 也就是说在“0A”之后的2个字节指向这个方法是属于哪个类的,紧接的2个字节指向这个方法的名称和类型,他们的值分别为:
- 0004:十进制4,表示指向常量池种的第4个常量
- 0011:十进制17,表示指向常量池中的第17个常量
- 根据上面javap查看的字节码内容,可以知道:
- 变量1表示方法引用表,为Object类的init构造方法
接着看第二个常量
8.常量池第二个常量.png- tag为09,为字段引用表为CONSTANT_Fieldref_info,其结果如下
CONSTANT_Fieldref_info{
u1 tag;
u2 class_index; 指向此字段的所属类
u2 name_type_index; 指向此字段的名称和类型
}
-
后面是4个字节都是索引
- 00 03:指向常量池中第3个常量
- 00 12:指向常量池中第18个常量
-
字节码表示
- 表示第二个常量为字段,且该字段属于Test类,字段名称为num,类型为I
其他常量也使用同样方式解析
4.访问标志
-
紧跟在常量池之后的常量是访问标志,占用两个字节
10.类索引与父类索引.png
- 访问标志代表类或者接口的访问信息:
- 比如该class文件是类还是接口,是否被定义为public,是否是abstract,如果是类,是否被声明为final等等。
- 各种访问标志如下所示:
访问标志 | 值 | 描述 |
---|---|---|
ACC_PUBLIC | 0X0001 | public类型 |
ACC_FINAL | 0X0010 | 被声明为final类型的类 |
ACC_SUPER | 0X0020 | 是否允许使用invokespecial字节码指令的新语义,默认为真 |
ACC_INTERFACE | 0X0200 | 标志这时一个接口类型 |
ACC_ABSTRACT | 0X0400 | 标志这是一个抽象类或是接口类型 |
ACC_ANNOTATION | 0X2000 | 标志这时一个注解 |
ACC_ENUM | 0X4000 | 标志这时一个枚举 |
- 我们定义的Test.java是一个普通的Java类,不是接口,枚举或注解,并且被public修饰,但没有被声明为final和abstract,因此它对应的access_flags为0021(0X0001和0X0020相结合)
5.类索引,父类索引与接口索引计数器
在访问标志后的2个字节是类索引,类索引后的2个字节就是父类索引,父类索引后的2个字节则是接口索引计数器,如下图:
10.类索引与父类索引.png- 可以看出类索引指向常量池中的第3个,父类索引指向常量池中的第4个索引,并且实现的接口个数为2个:
看字节码对应内容:
10.2类-父类-接口索引.png- 从图中可以看出,第3个常量和第4个常量均为CONSTANT_Class_info表类型,并且代表的类分别为“Test”和“Object”。
- 再看接口计数器的值为2,代表这个类实现了2个接口,查看在接口计数器之后的4个字节分别为:
- 0005:指向常量池中的第5个常量,从图中可以看出第5个常量值为Serializable
- 0006: 指向常量池中的第6个常量,从图中看出第6个常量值为Cloneable
- 综上所述,可以得出如下结论:当前类为Test继承自Object,并实现了 Serializable 和 Cloneable 这两个接口
6.字段表
- 紧跟在接口索引集合后面的就是字段表
- 字段表的主要功能是用来描述类或者接口中声明的变量
- 这里的字段包含了类级别变量以及实例变量,但不包含方法内部声明的局部变量
- 一个类中的变量个数是不固定的,因此在字段表集合之前还是使用一个计数器来表示变量的个数
- 0001表示类中声明了1个变量,所以字段计数器之后会紧跟着1个字段表的数据结构
字段表的具体结构为:
CONSTANT_Fieldref_info{
u2 access_flags 字段的访问标志
u2 name_index 字段的名称索引(也就是变量名)
u2 descriptor_index 字段的描述索引(也就是变量的类型)
u2 attributes_count 属性计数器
attribute_info
}
字段访问标志
11-1字节码-字段访问标志.pngJava类中的变量,也可以使用public,private,final,static等标识符进行标识。因此解析字段时,需要先判断它的访问标志,字段的访问标志如下:
字段访问标志 | 值 | 描述 |
---|---|---|
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 |
字段表结构图中的访问标志值为0002,代表它是private类型,变量名索引指向第7的常量;变量名类型索引指向第8个字段。根据class字节码文件内容可以知道,
11-1字节码-字段访问标志.png- 类中有一个名为num,类型为int类型的变量
7.方法表
字段表之后跟着的就是方法表常量。方法表常量应该也是以一个计数器开始的,因为一个类中的方法数量是不固定的。
12-2.add方法表内容.png 12.方法表.png
- 上图表示Test.class中有两个方法,我们声明了一个add方法,再加默认构造函数也被包含在方法表常量中
- 方法表的结构如下:
CONSTANT_Methodref_info{
u2 access_flags; 方法的访问标志
u2 name_index; 指向方法名的索引
u2 descriptor_index; 指向方法类型的索引
u2 attributes_count; 方法属性计数器
attribute_info attributes;
}
- 方法的访问标志如下:
字段访问标志 | 值 | 描述 |
---|---|---|
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 | 0X4000 | 方法是否为abstract |
- 主要看add方法
- 从图中可知add方法的具体信息
- access_flags = 0001 表示方法访问标志是public方法
- name_index = 000D 方法名指向常量池第13个位置,也就是add
- descriptor_index = 000E 方法类型指向常量池第14个位置,也就是(I)I,表示该方法接收int类型参数,并返回int类型出参
8.属性表
- 在前面解析字段和方法的时候,在他们的结构中都有看到有一个叫做attributes_count的表,这就是属性表
属性表并没有一个固定的结构,各种不同的属性只要满足以下结构即可:
CONSTANT_Attribute_info{
u2 name_index;
u2 attribute_length length;
u1[] info;
}
- Code属性表
继续查看刚才add方法中属性表内容:
13-1.属性表内容.png- 可以看到add方法类型索引后跟着的就是add方法的属性
- 0001 是属性计数器,代表只有一个属性
- 000B 是属性表类型索引,指向常量池中第11个常量位置,是一个Code属性表,如下:
13-3.add方法字节码Code属性表内容.png
- Code属性表中,主要是一系列的字节码,JVM执行add方法时,就通过这一系列指令来做相应的操作
总结
- 字节码由javac编译器编译生成,然叫交给java虚拟机进行解析执行
- 字节码主要由无符号数和表组成
- class字节码文件内容包括
- 文件标志魔数
- 版本号
- 常量池(重点)
- 类的访问标志
- 类索引,父类,接口索引
- 字段表
- 方法标志
- 属性表
网友评论