JVM(一) — Class 文件结构

一、概述

Class文件是一组以8位字节为单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件中,中间没有任何分隔符。 当遇到需要占用8位以上的数据项时,则会按照高位在前、低位在后 (Big-endian顺序) 的方式来分割成多个8位字节进行存储。

根据Java虚拟机规范的规定,Class文件格式采用一种类似C语言结构体的伪结构(ClassFile)来存储数据。这种伪结构中只有两种数据类型:无符号数

  • 无符号数:属于基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值、字符串值。
  • 表:由多个无符号数或者子表作为数据项构成的符合数据类型。用于描述有层次关系的复合结构的数据。整个Class其实就是一张表。

注意:

  • 任何一个Class文件都对应着唯一的类或接口的定义信息;
  • 类或接口并不一定定义在文件里,也可以通过类加载器直接生成。

关联文章:

二、ClassFile 的结构

Class文件格式采用一种类似C语言结构体的伪结构(ClassFile)来存储数据。
链接:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1

ClassFile {
    u4             magic;               //魔数,固定值0xCAFEBABE
    u2             minor_version;       //次版本号
    u2             major_version;       //主版本号
    u2             constant_pool_count; //常量个数
    cp_info        constant_pool[constant_pool_count-1];  //常量池内容
    u2             access_flags;        //类访问标识
    u2             this_class;          //当前类索引
    u2             super_class;         //父类索引
    u2             interfaces_count;    //接口个数
    u2             interfaces[interfaces_count];  //具体的接口内容
    u2             fields_count;                  //字段个数
    field_info     fields[fields_count];          //具体的字段内容
    u2             methods_count;                 //方法个数
    method_info    methods[methods_count];        //具体的方法内容
    u2             attributes_count;              //属性个数
    attribute_info attributes[attributes_count];  //具体的属性内容
}

三、ClassFile 的组成部分

3.1 魔数

每个Class文件头4个字节称为魔数(Magic Number),作用是用于确定这个Class文件是否能被虚拟机所接受,Class文件的魔数固定值为 0xCAFEBABE。很多文件存储标准中都使用魔数来进行身份识别(文件后缀容易被人为更改),例如图片格式(gif、png等)。

3.2 版本号

紧跟魔数的4个字节存储的是Class文件的版本号(主版本号,次版本号),即第5个字节-第8个字节。
次版本号:第5,6字节
主版本号:第7,8字节

3.3 常量池

常量池可以理解为Class的资源仓库,它是Class文件空间最大的数据项之一,长度不固定,同时它还是在Class文件中第一个出现的表类型数据项目

  • 常量池中常量的数量不固定,所以需要一个u2类型的数据来计算常量池的大小。
  • 常量池的大小 = u2的大小 - 1(常量池从1开始计数),当 u2 的大小为0时,代表没有使用常量池。

所有常量池类型都具有以下通用格式:

cp_info {
    u1 tag;  
    u1 info[];
}
3.3.1 常量池中存放的常量类型: 字面量符号引用
  • 字面量:与Java语言层面的常量概念相近,如文本字符串、声明为final的常量值等。
  • 符号引用:编译语言层面的概念,包括以下3类:
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符
3.3.2 常量池的项目类型

常量池中的每个项目都必须以一个 1 字节的标记开头,代表 cp_info 的类型。 info 数组的内容随 tag 的值而变化。常量池的项目类型如下图所示:
constant_type
示例:
CONSTANT_Class_info 结构用于代表类或接口类型,格式如下:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

更多常量池中类型结构表可以查看链接:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4

3.3.3 通过javap查看class文件字节码内容

Class文件都是二进制格式,可通过JDK中的javap工具分析Class文件字节码。
命令:javap -v -p file_path/Test.class,更多用法可通过javap --help查看。

3.4 (类/接口)访问标识

2个字节代表访问标志(紧随常量池之后),标志用于识别一些类或者接口层次的访问信息。

标识名标识值解释
ACC_PUBLIC0x0001声明为public;
ACC_FINAL0x0010被声明为final;不允许子类修改
ACC_SUPER0x0020当被invokespecial指令调用时,将特殊对待父类的方法
ACC_INTERFACE0x0200接口标识符
ACC_ABSTRACT0x0400声明为abstract;不能被实例化
ACC_SYNTHETIC0x1000声明为synthetic;不存在于源代码,由编译器生成
ACC_ANNOTATION0x2000声明为注释类型
ACC_ENUM0x4000声明为枚举类型

3.5 类/父类索引

类索引和父类索引都是一个u2类型的数据,由于Java语言是单继承,故父类索引只有一个。除了java.lang.Object对象的父类索引为0,其他所有类都有父类。

3.6 接口索引

一个类可以实现多个接口,故利用interfaces_count和interfaces[interfaces_count]来描述接口信息。如果该类没有实现任何接口,则interfaces_count=0,接口的索引表不再占用任何字节。

  • interfaces_count 用来记录该类所实现的接口个数。
  • interfaces[interfaces_count] 用来记录所有实现的接口内容。

3.7 字段表

字段表用于描述接口或者类中声明的变量。字段包括类级变量实例级变量,不包含局部变量。

每个字段由一个 field_info 结构来描述,格式如下:

field_info {
    u2             access_flags;     //访问标识
    u2             name_index;       //名称索引
    u2             descriptor_index; //描述符索引
    u2             attributes_count; //属性个数
    attribute_info attributes[attributes_count];  //属性表的具体内容
}

字段访问标识如下:

标识名标识值解释
ACC_PUBLIC0x0001声明为 public; 可以从包外部访问
ACC_PRIVATE0x0002声明为 private; 只有定义的类可以访问
ACC_PROTECTED0x0004声明为 protected;只有子类和相同package的类可访问
ACC_STATIC0x0008声明为 static;属于类变量
ACC_FINAL0x0010声明为 final; 对象构造后无法直接修改值
ACC_VOLATILE0x0040声明为 volatile; 不会被缓存,直接刷新到主屏幕
ACC_TRANSIENT0x0080声明为 transient; 不能被序列化
ACC_SYNTHETIC0x1000声明为 synthetic; 不存在于源代码,由编译器生成
ACC_ENUM0x4000声明为enum

注意事项:

  • 实际情况中,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED 三个标志只能出现一个。
  • ACC_FINAL,ACC_VOLATILE不能同时选择。
  • 接口中的字段默认包含ACC_PUBLIC、ACC_STATIC、ACC_FINAL 标识。

3.8 方法表

每个方法(构造方法和类或接口初始化的方法) 都由一个 method_info 来描述,格式如下:

method_info {
    u2             access_flags; //访问标识
    u2             name_index;  //名称索引
    u2             descriptor_index;  //描述符索引
    u2             attributes_count;  //属性个数
    attribute_info attributes[attributes_count]; //属性表的具体内容
}

方法访问标识如下:

标识名标识值解释
ACC_PUBLIC0x0001声明为 public; 可以从包外部访问
ACC_PRIVATE0x0002声明为 private; 只有定义的类可以访问
ACC_PROTECTED0x0004声明为 protected;只有子类和相同package的类可访问
ACC_STATIC0x0008声明为 static;属于类变量
ACC_FINAL0x0010声明为 final; 不能被覆写
ACC_SYNCHRONIZED0x0020声明为 synchronized; 同步锁包裹
ACC_BRIDGE0x0040桥接方法, 由编译器生成
ACC_VARARGS0x0080声明为 接收不定长参数
ACC_NATIVE0x0100声明为 native; 由非Java语言来实现
ACC_ABSTRACT0x0400声明为 abstract; 没有提供实现
ACC_STRICT0x0800声明为 strictfp; 浮点模式是FP-strict
ACC_SYNTHETIC0x1000声明为 synthetic; 不存在于源代码,由编译器生成
  • 方法的定义通过访问标志、名词索引、描述符索引表达。
  • 方法中的Java代码经过编译器编译成字节码指令后,存放在方法属性表集合中的“Code”属性中。
  • 如果子类没有重写(Override)父类方法,则方法集合中不会出现父类的方法信息。
  • Java语言中,要重载(Overload)一个方法必须满足2个条件:方法名必须与原方法同名,特征签名不同(特征签名是指方法中各个参数在常量池的字段符号引用的集合,不包括返回值。)。

3.9 属性表

属性表可以出现在Class文件、字段表、方法表中,格式如下:

attribute_info {
    u2 attribute_name_index;   //属性名索引
    u4 attribute_length;       //属性长度
    u1 info[attribute_length]; //属性的具体内容
}

属性表的限制相对宽松,不需要各个属性表有严格的顺序,只要不与已有的属性名重复,任何自定义的编译器都可以向属性表中写入自定义的属性信息,Java虚拟机运行时会忽略掉无法识别的属性。

虚拟机中预定义21项属性,下面介绍几种常见的属性,如下表所示:

属性名使用位置解释
Code方法表方法体的内容
ConstantValue字段表final关键字定义的常量值
Deprecated类、方法表、字段表声明为deprecated
Exceptions方法表方法抛出的异常
InnerClasses类文件内部类的列表
LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量描述
LocalVariableTypeTable使用特征签名代替描述符,是为了引入泛型语法后能描述泛型参数化类型而添加的。
Signature类、方法表、字段表用于支持泛型的方法签名,由于Java的泛型采用擦除法,避免类型信息被擦除后导致签名混乱,Signature记录相关信息
3.9.1 Code 属性

Java程序方法体中的代码经过Javac编译器处理后,得到的字节码指令存储在Code属性内,Code属性位于方法表的属性集合中。但并非所有的方法表都必须存在这个属性,如接口或抽象类的方法就不存在Code属性。

Code_attribute 的格式如下:

Code_attribute {
    u2 attribute_name_index; //常量池中的uft8类型的索引,值固定为”Code“
    u4 attribute_length;     //属性值长度,为整个属性表长度-6
    u2 max_stack;   //操作数栈的最大深度值,jvm运行时根据该值佩服栈帧
    u2 max_locals;  //局部变量表最大存储空间,单位是slot
    u4 code_length; //字节码指令的个数
    u1 code[code_length];      //具体的字节码指令
    u2 exception_table_length; //异常的个数
    {   u2 start_pc;     //起始pc
        u2 end_pc;
        u2 handler_pc;   //当字节码在[start_pc, end_pc)区间出现catch_type或子类,则转到handler_pc行继续处理。
        u2 catch_type;   //当catch_type=0,则任意异常都需转到handler_pc处理
    } exception_table[exception_table_length];   //具体的异常内容
    u2 attributes_count;                         //属性的个数
    attribute_info attributes[attributes_count]; //具体的属性内容
}

slot 是虚拟机为局部变量分配内存使用的最小单位。对于byte/char/float/int/short/boolean/returnAddress等长度不超过32位的局部变量,每个占用1个Slot;对于long和double这两种64位的数据类型则需要2个Slot来存放。

3.9.2 LineNumberTable 属性

LineNumberTable 属性用于描述Java庅行号与字节码行号之前的对应关系。如果不生成LineNumberTable 属性,当程序出现异常时,堆栈中将不会显示出错的行号。

LineNumberTable_attribute 格式如下:

LineNumberTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 line_number_table_length;
    {   u2 start_pc;    //字节码行号
        u2 line_number;	//Java源码行号
    } line_number_table[line_number_table_length];
}
3.9.3 LocalVariableTable 属性

LocalVariableTable 属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。如果不生成LocalVariableTable 属性,其它人引用这个方法时,参数名会消失,用类似arg0、arg1来代替;同时调试启舰无法通过参数名来获取值。

LocalVariableTable_attribute 格式如下:

LocalVariableTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 local_variable_table_length;
    {   u2 start_pc;
        u2 length;
        u2 name_index;
        u2 descriptor_index;
        u2 index;     //
    } local_variable_table[local_variable_table_length];
}

LocalVariableTypeTable 结构中用字段的特征签名(signature) 来描述。
LocalVariableTypeTable_attribute 格式如下:

LocalVariableTypeTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 local_variable_type_table_length;
    {   u2 start_pc;
        u2 length;
        u2 name_index;
        u2 signature_index;  //与LocalVariableTable_attribute结构唯一的区别
        u2 index;
    } local_variable_type_table[local_variable_type_table_length];
}
3.9.4 ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。

实例变量:
在实例构造器方法进行赋值。

类变量:
在类构造器方法进行赋值 或 使用ConstantValue属性来赋值。

  • 如果变量由final和static同时修饰(即常量),并且该变量的数据类型是基本类型或String类型,就生成ConstantValue属性来进行初始化。
  • 如果变量没有被final修饰,或并非基本类型及字符串,则将会选择在方法中进行初始化。

ConstantValue_attribute 结构格式如下:

ConstantValue_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 constantvalue_index;
}

参考资料

  1. 《深入理解Java虚拟机》
  2. Java 虚拟机规范:https://docs.oracle.com/javase/specs/jvms/se8/html/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值