class文件是以一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前[插图]的方式分割成若干个8个字节进行存储。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“表”。
- 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
- 表:由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。
示例demo
后续我们以以下代码对应的class文件做分析,编译采用JDK8
package test;
/**
* @Author : Wugao
* @Date : Created in 16:06 2020/7/1
* @Modified By :
*/
public class DemoTest {
private static final String ABC = "1998";
private int age;
private String name;
public static void main(String[] args) {
System.out.println(ABC);
DemoTest test = new DemoTest();
test.age += 1;
}
public String getName(String name){
return name;
}
public int getAge(){
return age;
}
}
魔数与Class文件的版本
- 定义:每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件
- java的魔术:0xCAFEBABE
- java版本号从45开始
- 紧接着魔数的第5,6个字节代表java次版本,第7,8个字节代表java主版本,如下图:0000代表次版本号为0,0034代表主版本号为52
- 对应的java版本,上面的文件代表是由jdk8编译的
常量池
紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目。
- 常量池入口放置一个u2类型的数据,代表常量池容量计数值(constant_pool_count),容量计数从1开始,也就是值为1时,容量为0,值为10时,容量为9(偏移地址:0x00000008),如上面的class为48,容量则为47
- 主要存放两大类常量
- 字面量:如文本字符串、被声明为final的常量值
- 符号引用:
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
- 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-ComputedConstant)
- 常量池入口之后是常量项
- 常量项的第一位是u1的标志位,代表接下来的常量的类型(共17种),如上面的class中为7,对应就是类或这个接口的符号引用
- CONSTANT_Class_info型常量的结构
- tag为标志位
- name_index是常量池的的索引值,此处为0x0002,查询这个索引位置的常量的标识为01,查表得知为CONSTANT_Utf8_info
- CONSTANT_Utf8_info
- length(u2): 字符串长度,字节数,最大值2^16 - 1,因此类名或方法名最大字节长度不能大于这个值,否则编译失败
- bytes:length个字节的字节码
- CONSTANT_String_info字符串类型字面量
- index(u2): 指向字符串字面量的索引,
- 所有17种常量池类型结构总表如下,需要时可以直接查表。另外可以使用javap -verbose Test.class查看类结构
访问标志
- 紧接着常量池的是访问标识,u2,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final;等等。
- 本例中是public,因此ACC_PUBLIC,ACC_SUPER为真,一刹那0x0001|0x0020=0x0021
类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合
此处类索引和父类索引分别是0001和0003,即常量池中索引为1和3的类型
接下来的u2是接口的个数,此处为0,如果不为0,后续将跟上N个接口索引指向常量池中的类型
字段表集合
- 接下来的两个字节代表字段的个数
- 然后是字段信息,Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量,此处为0003,包括ABC, age,name3个字段
- 字段访问标识(access_flags)
- ACC_PUBLIC,ACC_PRIVATE,ACC_PROTECTED只能选择其中一个
- ACC_FINAL、ACC_VOLATILE不能同时选
- 简单名称索引(u2):指向常量池中的utf-8字符串
- 描述符索引(u2):对于字段的描述符指向常量池中字段的类型
- 对于字段或者方法,可以带有属性,接下来跟着的是属性的个数(u2),后面根据属性的不同,属性的接口也不同,如此处的ABC,会带有一个ConstantValue属性,结构为属性名索引,属性长度固定2,属性值索引
方法表集合
- 跟字段表集合类似,首先是方法的个数,然后是方法的信息
- 方法结构表包括两个部分,方法基本信息,属性
- access_flags: 方法访问标识
- name_index: 简单方法名的常量池索引
- descriptor_index: 描述符的常量池索引,这里描述了方法的参数,返回值,比如有一个方法是java.lang.Boolean check(String abc),那么此处对应的常量应该是(Ljava/lang/String;)Z
规则是【左括号 + 参数类型 + 右括号 + 返回值类型】这里为什么是Z呢?遵循以下规则
- 举例:该类有4个方法,为什么有四个方法呢?我们代码中只有main方法,getName,getAge方法,是因为每个函数在没有申明有参构造器时都有一个隐藏的默认构造方法
-
0001(1)代表该方法的方法访问标识符:查表得知是ACC_PUBLIC
-
000d(13)代表方法的简单名称的常量索引,查询得知是
-
000e(14)代表访问描述符常量池索引,查询得知是()V,表示无参,无返回
-
0001(1)代表有一个属性
-
000f(15)代表属性名称索引,查询得知是Code,Code属性表结构如下
-
0000002f(37):代表属性长度
-
0001(1):操作数栈最大深度,在执行这个方法时,操作数栈都不会超过这个深度
-
0001(1):局部变量表所需的存储空间,这里只变量槽的数量
-
0000 0005(5):代表JVM指令长度字节数,后续跟这个的5个字节就是5个指令
-
2a,b7,00,10,b1:分别代表5个指令,可通过查询虚拟机字节码指令表获得
-
0000(0):代表显示申明的Exception的个数,这里为0,如果不为0
- 如果存在异常表,那它的格式应如所示,包含四个字段,这些字段的含义为:如果当字节码从第start_pc行[插图]到第end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转到handler_pc处进行处理
-
0002(2):代表属性的个数
-
0012(18):代表属性名称对应常量池索引,这里是LineNumberTable
- 0000 0006:代表属性长度
- 0001:代表属性表长度,后面跟着N个line_number_info,这个line_number_info有两个u2组成,第一个是字节码行号,第二个是java源码行号
- 0000和0008分别代表字节码的0行对应java源码的第8行
-
0013(19):代表属性名称对应常量池索引,这里是LocalVariableTable
- 0000 000c(12):代表属性长度
- 0001代表本地变量表的长度,跟着n个local_variable_info,这个表的结构有u2:start_pc,u2:length,u2:name_index,u2:descriptor_index,u2:index
- 0000,0005,0014,0015,0000:分别表示局部变量生命周期开始的字节码偏移量,作用范围覆盖的长度,名称常量池索引,描述符常量池索引,占用变量槽的位置
-
至此,一个方法分析就完了,后面跟着的就是类中的其他方法,方法是所有类型中最复杂的,学会了这个,其他应该就简单了。
-
-