本文旨在讲解class文件的整体结构信息,阅读本文后应该可以完整的了解class文件的格式以及各个部分的逻辑组成含义
class文件包含了java虚拟机指令集 和 符号表 以及若干其他辅助信息.
class文件是一组以8位字节为基础单位的二进制字节流 各个数据项按照顺序紧凑的排列在Class文件中,中间没有任何分隔符号 class文件采用类似 c结构体的格式存储数据 数据类型只有两种 无符号数 和 类c结构体的 表 表是由无符号数或者其他的表构成的
整个class文件就是一张表 无论无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器用于指示接下来的数据个数,然后是若干个连续的数据项
class文件主要内容为: 类本身的信息 字段 方法 常量池 以及方法中的Code属性 再就是一些相关的辅助信息 类本身的信息类本身有一些必备的描述信息,比如类名 访问修饰符 继承关系等 字段用于描述接口或者类中声明的变量 字段包括类变量以及实例变量,不包括局部变量 他有访问标志 名称 描述符信息 方法用于描述方法表信息 类似字段 也有访问标志 名称 描述符信息 常量池可以理解为Class文件的资源仓库,所以他是与其他项目关联最多的数据类型 主要是两大类: 字面量 以及符号引用 字面量接近java语言层面的常量概念 比如文本字符串 声明为final常量的值 符号引用包括: 类和接口的全限定名 字段的名称和描述符 方法的名称和描述符 虚拟机加载class文件的时候动态链接,所以class文件中不会保存方法的最终内存布局, 还需要转换 虚拟机运行时从常量池中获得对应的符号引用,然后在创建或者运行时解析翻译到具体的内存地址中 Code属性存放的Java方法的内容,位于方法method_info 内 存放的是java方法经过编译器编译成的字节码指令 说白了存放的就是代码的编译后形式
概述: class文件作为JVM的"机器语言" 主要包括两部分的信息,基础信息以及附加信息 基础信息为源代码中呈现的信息 类自身信息/字段/方法用于描述源代码中的类/字段/方法 常量池中保存了资源信息,比如字段的名字 方法的描述符等 方法中的code属性保存了方法中的代码的执行逻辑 额外信息为虚拟机执行过程中或者字节码指令执行所需要的信息 为了保证虚拟机能够正确的加载class文件 另外虚拟机加载类还需要做一些额外的工作比如校验信息等 字节码指令的执行可能还需要一些额外的信息 这些额外的信息通常也是保存在常量池中或者以属性的形式出现 |
所以想要理解class文件的内容,就需要从这两个方面出发
基础信息以及额外附加信息
基础信息是对于源代码的映射
给出任意一段java代码,源代码中到底有哪些信息呢?
比如
类的全限定名 类的属性信息 访问权限等
字段的名称 类型信息 访问权限等
方法的名称 方法签名 返回值类型 访问权限等
类或者方法或者字段 有没有注解?
类的父类是什么?
类继承了哪些接口? 等等等等
其实换句话说你所有使用依赖的功能,都需要有底层的数据来支撑
额外的附加信息主要涉及到字节码指令以及虚拟机加载相关原理,额外的附加信息是附属于基本信息的,他们渗透在基本信息里面
所以下面的说明中,我们以基础信息为纲要,涉及到的附加信息会说明或者从功能上看出来是作为附加信息
class文件包含了虚拟机所需要知道的,关于类或者接口的所有信息
结构体
他的基本数据类型为无符号数,以及表 表 是 数据组织结构类似于C语言中的结构体的的一种形式
为了更好地理解这种形式的逻辑,不了解C语言的,可以稍微了解一点结构体的形式,更有利于理解class文件的数据形式
struct 结构体名 { 类型名1 成员名1; 类型名2 成员名2; ..... 类型名n 成员名n; }; |
比如
struct student
{
char name[10];
char sex;
int age;
float score;
};
他的每个元素的数据类型都可以不相同
而且每个字段本身也可以是指向另外一个数据项的地址
也类似与数据库中的关联字段ID,这个ID在另一个表中有代表一整条的记录
比如学生表有addressId字段,用于关联地址信息
地址是一条完整的记录,其中可能包括 国家地区 省市 乡镇等等字段值
每一个class文件都是由字节流组成
一个字节8位
所有的16位 32位 和 64位数据长度都可以通过构造成2个 4个或者8个字节来表示
多字节的数据将会大端排序(高位字节在地址最低位 )
ps: 所谓大小端 |
对于字节的描述,java虚拟机规范中使用u1 u2 u4 u8来分别表示1,2,4和8 个字节的无符号数
基本数据类型为u1,u2,u4,u8
复杂的数据类型由类似结构体的形式来组织无符号数或者是类结构体的形式 可以称之为 表 也就是说表 里面可以有表
比如常量池表中的数据结构为
cp_info{
u1 tag;
u1 info[ ]
}
所以说
class文件的形式是一张巨大的表,是一个二进制字节流 只有两种数据表示形式 无符号数 以及 表(结构体 复合的数据结构) 各个数据项严格的按照顺序存放,之间没有填充或者对齐,这也是为何编译后代码如此紧凑的原因之一 基本数据类型为: u1 u2 u4 u8 |
ClassFile {
u4 magic;
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];
}
class文件是一张表
这张表里面 记录了class文件本身的相关信息 比如magic 以及版本号
class文件中类索引 this_class 父类索引 super_class 以及接口集合 interfaces 三个数据项确定了类的继承关系
同时又记录了字段 方法 以及属性信息
还是以之前的例子为例
源代码
public class HelloWorld {
private int x;
private String y;
public void fun() {
}
public static void main(String[] args) {
System.out.println("hello world");
}
}
使用Javap解析后的数据
使用WinHex打开.class 文件 的部分结果
我们对照着class文件的结构进行解析
注意:
上图中一列是一个字节 8位 也就是相当于u1 4位表示一个十六进制数
所以一列表示两个十六进制数
第一项 u4 magic 复合class文件魔数设置0xCAFEBABE |
第二项 u2 minor_version 第三项 u2 major_version 所以说 主版本号为 52(34是十六进制) 次版本号为0 与javap解析后的数据吻合 |
第四项 u2 constant_pool_count 十六机制27 十进制39 可以看到javap解析后的Constant pool:中总共有从#1 到 #38 常量池计数器constant_pool_count的值等于常量表中的成员数加1 常量池标的索引值只有大于0 且小于constant_pool_count时才有效 所以此处解析也是对的 |
不在继续一一比对,你可以使用javap 进行查看
这是官方提供的class文件的解析工具
总结:
class文件的存储形式就是一个二进制字节流
使用类似结构体的形式
将源代码映射的基础信息以及运行时必要的辅助信息,而这些基础信息也都已经被分割为最小的数据单位
进行合理紧凑的组织起来
class文件详解
ClassFile {
u4 magic;//唯一作用是确定这个文件是否为一个能被虚拟机所接受的class文件。魔数值固定为0xCAFEBABE,不会改变
u2 minor_version;//唯一作用是确定这个文件是否为一个能被虚拟机所接受的class文件。魔数值固定为0xCAFEBABE,不会改变
u2 major_version;//主版本号
u2 constant_pool_count;//常量池计数 值等于常量池表中的成员个数加1
cp_info constant_pool[constant_pool_count-1];//常量池 1~ constant_pool_count-1 为索引
u2 access_flags;//访问标志以及类型信息
u2 this_class;//当前类索引 指向常量池中一个CONSTANT_Class_info
u2 super_class;//父类索引 0 或者指向常量池中一个CONSTANT_Class_info
u2 interfaces_count;//直接超接口数量
u2 interfaces[interfaces_count];//接口表
u2 fields_count;//字段个数 static类变量或者非sttic的实例变量 不包括继承的
field_info fields[fields_count];//字段表
u2 methods_count;//方法个数 所有方法 但不包括继承而来的
method_info methods[methods_count];//方法表
u2 attributes_count;//属性个数
attribute_info attributes[attributes_count];/属性表
}
从class文件的数据结构上来看,主要有下面几部分信息内容
class文件本身的信息 magic minor_version minor_version
类本身的信息 access_flags this_class super_class interfaces_count interfaces[interfaces_count]
常量信息 constant_pool_count constant_pool[constant_pool_count-1]
字段 fields_count fields[fields_count]
方法 methods_count methods[methods_count]
属性 attributes_count attributes[attributes_count]
在进入更加详细的说明之前,有一部分内容必须提前说一下
那就是一些内部名称数据的表示形式
就好像编码一样,亦或者理解成书写格式与规范,比如我们会把姓写在名的前面
对于class文件中描述的一些信息,我们有固定的格式的信息描述符号
比如下面会提到的我们用 D表示是double类型
主要涉及两部分内容 名称 和描述符
名称描述
类和接口的二进制名称
class文件中的类和接口的二进制名称 是通过全限定名称来进行表示的
称之为二进制名称
注意,全限定名的分割形式不再是 英文句号 . 而是 / 比如 java/lang/System
非限定名
方法名 字段名 局部变量名以及形式参数的名都采用非限定名的形式
描述符
分为字段描述符/方法描述符
字段描述符
上面截图自The Java Virtual Machine Specification, Java SE 8 Edition
他表示字段描述符使用FieldType来进行表述
FieldType有包括基本类型/对象类型/数组类型
形式是 FieldType |
B | byte | [基本类型] 有符号的字节数组 |
C | char | [基本类型] 基本多语种平面中的Unicode代码点 UTF-16 |
D | double | [基本类型] 双精度浮点数 |
F | float | [基本类型] 单精度浮点数 |
I | int | [基本类型] 整型数 |
J | long | [基本类型] 长整数 |
S | short | [基本类型] 有符号短整数 |
Z | boolean | [基本类型] 布尔值true/false |
L ClassName; | L ClassName; | [对象类型] ClassName类的实例 |
[ | reference | [数组类型] 一维数组 |
比如int 为I Object 为 L java/lang/Object; double[][][] d 为 [[[D
方法描述符
上面截图自The Java Virtual Machine Specification, Java SE 8 Edition
他表示一个方法描述符由一个参数描述符ParameterDescriptor 和一个返回类型描述符ReturnDescriptor组成
参数类型描述符是: FieldType 类型
返回类型描述符是: FieldType类型或者Void类型VoidDescriptor
Void类型描述符 使用的是V来进行表示
形式是 ( {ParameterDescriptor} ) ReturnDescriptor 注意: {} 不是一部分,是想表达和数组似的,也可能是多个 |
比如
Object m(int i, double d, Thread t) {...}
他的描述符为:
(IDLjava/lang/Thread;)Ljava/lang/Object;
类本身的信息
access_flags
this_class super_class interfaces_count interfaces[interfaces_count]
名称 | 值 | |
ACC_PUBLIC | 0x0001 | 声明为public 包外访问 |
ACC_FINAL | 0x0010 | final 不允许子类 |
ACC_SUPER | 0x0020 | 调用invokespecial 需要处理父类 |
ACC_INTERFACE | 0x0200 | 这是一个接口 |
ACC_ABSTRACT | 0x0400 | abstract 抽象的不能被实例化 |
ACC_SYNTHETIC | 0x1000 | class文件并非由java源代码生成 |
ACC_ANNOTATION | 0x2000 | 注解 |
ACC_ENUM | 0x4000 | 枚举 |
access_flag 字段为类的访问权限标志以及类型值
this_class super_class interfaces[interfaces_count] 构成了类的继承关系 指向的都是常量池中的CONSTANT_Class_info
对于super_class来说,可能为0 因为Object没有父类,其他所有都需要有对应的值存在
主要分为两类 字面量 符号引用 字面量类似java语言层面的含义 文本字符串 声明为final 的常量值 符号引用包括: 类和接口的全限定名 字段的名称和描述符 方法的名称和描述符
常量池包含了class文件结构及其子结构中引用的所有的,字符串常量,类或者接口名,字段名,以及其他常量 |
class文件由无符号数和表结构两种类型组成
关于名称的书写规范格式上面已经进行了描述
但是怎么表述这些名称的"字符串" 形式呢? 比如className = "Helloworld" 怎么保存Helloworld?
另外一些基本的数据类型的数据在class文件中又将是如何存放呢?比如 int类型的x=5 这个5又怎么保存?
字符串以及不同数值类型也都不就是一个u1 所以也需要组织形式也就是数据结构 也就是表
常量池中的表结构的类型可以分为三种类型
基本数据类型,比如 int long的描述形式, 虽然class文件是二进制字节流,最小为u1 但是这些基本数据类型在逻辑意义上来说,才是最小的描述单位 |
用于表述, 用于描述各个部分包含的逻辑内容的表 "结构体" 复合形式的数据类型结构 |
中间的映射结构表 相当于数据库中的中间关系表 |
另外所有的常量池中的数据都是cp_info形式,所有的常量池中的数据都完全遵循这个格式
cp_info{
u1 tag;
u1 info[];
}
只不过info[]具体的格式,由tag来进行决定
也就是说不同的tag 那一块区域的大小以及表示的含义自然是不同的
其实tag 跟我们平时写代码时候的类型是一个道理 就是接下来这块区域的内容的类型标记
tag值如下:
也就是总共有下面这些类型的常量池信息
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
常量池中的基础数据类型部分
我们先说下常量池中的封装好的数据类型部分
字符串常量 |
CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; } |
tag是CONSTANT_Utf8 1 字符串采用改进过的UTF-8编码表示 接下来是编码后的字符串占用字节数以及字符串 class文件中的方法字段名称都是此类型 |
int整型 4字节 |
CONSTANT_Integer_info { u1 tag; u4 bytes; } |
tag为CONSTANT_Integer 3 大端排序的int值 |
单精度浮点型 float 4字节 |
CONSTANT_Float_info { u1 tag; u4 bytes; } |
tag为CONSTANT_Float 4 大端排序 IEEE754单精度格式 的floa值 |
long与double 是64位 8个字节
分为4个高字节和4个低字节
long 长整型 8字节 |
CONSTANT_Long_info { u1 tag; u4 high_bytes; u4 low_bytes; } |
tag为CONSTANT_Long 5 大端排序的long值 |
双精度浮点型 double 8字节 |
CONSTANT_Double_info { u1 tag; u4 high_bytes; u4 low_bytes; }
|
tag为CONSTANT_Double 6 大端排序的 double值 |
除了基本数据类型其他的就是复合数据类型
所有的复合数据类型基本上都会包含其他的数据结构
这种包含方式使用的就是索引 #xxx的形式, 指向常量池中的另外一项数据
常量池中的中间关系映射类型部分
CONSTANT_NameAndType_info { u1 tag; u2 name_index; u2 descriptor_index; } |
tag为 CONSTANT_NameAndType (12) NameAndType 就是名称和类型的意思 对于方法 / 字段 来说, 他们都有变量名称或者方法名称 他们也都有变量类型和方法签名(方法的类型) NameAndType 是作为一个中间表形式的数据结构 字段/方法中都有一个索引指向他,他又指向了实际的名称和类型 不管是方法名称还是字段名称 不管是方法签名还是字段类型都是字符常量的形式 name_index 和 descriptor_index 指向的都是CONSTANT_Utf8_info |
常量池中的复合数据类型部分
String类型的常量对象 |
CONSTANT_String_info { u1 tag; u2 string_index; } |
tag为CONSTANT_String 8 他表示的是String类型的数据,我们知道String是常量 字符串常量是用CONSTANT_Utf8_info进行表示的 所以 String_index指向的就是对应的CONSTANT_Utf8_info的"行号" |
方法类型 |
CONSTANT_MethodType_info { u1 tag; u2 descriptor_index; } |
CONSTANT_MethodType 16 CONSTANT_NameAndType_info 是一个用于字段或者方法结构中的中间结构,包含了名称和类型 CONSTANT_MethodType_info 仅仅表示的就是方法签名 方法签名对应的是CONSTANT_Utf8_info 所以descriptor_index 指向 方法类型描述符的CONSTANT_Utf8_info |
类或接口 |
CONSTANT_Class_info { u1 tag; u2 name_index; } |
tag 为CONSTANT_Class 7 名称自然是字符串常量也就是CONSTANT_Utf8_info 所以 name_index指向常量池中的 CONSTANT_Utf8_info |
字段 |
CONSTANT_Fieldref_info { u1 tag; u2 class_index; u2 name_and_type_index; } |
CONSTANT_Fieldref 9 class_index 表示当前字段 对应或者说所属的 类或者接口 类和接口都可能 class_index指向CONSTANT_Class_info name_and_type_index 表示当前字段的名称和类型 name_and_type_index指向CONSTANT_NameAndType_info |
方法 |
CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; } |
CONSTANT_Methodref 10 class_index 表示当前方法 对应或者说所属的类,必须是类,不能是接口 class_index指向CONSTANT_Class_info name_and_type_index 表示当前方法的名称和方法签名 name_and_type_index指向CONSTANT_NameAndType_info |
接口方法 |
CONSTANT_InterfaceMethodref_info { u1 tag; u2 class_index; u2 name_and_type_index; } |
CONSTANT_InterfaceMethodref 11 class_index 表示当前方法 对应或者说所属的接口,必须是接口 不能是类 class_index指向CONSTANT_Class_info name_and_type_index 表示当前方法的名称和方法签名 name_and_type_index指向CONSTANT_NameAndType_info |
方法调用 |
CONSTANT_MethodHandle_info { u1 tag; u1 reference_kind; u2 reference_index; } |
CONSTANT_MethodHandle 15 方法调用,顾名思义也就是描述 方法的调用 对于一个方法调用来说,方法可能有不同的类型,不同的类型有不同的操作对象 reference_kind 正是描述方法的调用类型 reference_index 描述的是方法的操作目标 reference_kind 的值为1~9 他的类型决定了方法句柄的类型 句柄类型的值表示方法句柄中字节码行为 |
用于表示invokedynamic指令 |
CONSTANT_InvokeDynamic_info { u1 tag; u2 bootstrap_method_attr_index; u2 name_and_type_index; } |
tag为CONSTANT_InvokeDynamic 18 CONSTANT_InvokeDynamic_info是为了字节码指令 invokedynamic 使用的 invokedynamic是为了更好的支持动态类型语言,Java7通过JSR292给JVM增加的一条新的字节码指令 bootstrap_method_attr_index 的值必须是对当前Class文件中引导方法表的bootstrap_methods[] 数组的有效索引 name_and_type_index 指向CONSTANT_NameAndType 表示方法名和方法描述符 |
字段表field_info 用于描述接口或者类中声明的变量
包括类变量 以及 实例变量 不包括方法内部声明的局部变量
可以包括的信息包括
字段 |
field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; } |
每个字段都由field_info结构定义 同一个class文件中不会有两个字段同时具有相同的字段名和描述符 access_flags 表示字段访问权限和基本属性 name_index指向字段的名字 CONSTANT_utf8_info descriptor_index 指向字段描述符CONSTANT_utf8_info 字段field 包含属性表,属性表结构的情况稍后介绍 |
access_flags字段类型
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_SYNTHETIC | 0x1000 | 字段是否由编译器产生 |
ACC_ENUM | 0x4000 | 字段是否为enum |
如同源代码中abstract不能和final同时使用,此处的标志位规则也是如此,有些标志是互斥的
一个字段最多只能设置ACC_PUBLIC ACC_PRIVATE ACC_PROTECTED 的一种
不能同时设置ACC_FINAL 和 ACC_VOLATILE
接口中所有字段都具有 ACC_PUBLIC ACC_STATIC ACC_FINAL 也可以设置ACC_SYNTHETIC 其他的都不行了
方法 |
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; } |
所有方法,包括实例初始化方法以及类或者接口初始化方法 一个class文件中不会有两个方法具有相同的方法名和描述符 name_index 指向方法名字 CONSTANT_Utf8_info descriptor_index 表示方法描述符 指向 CONSTANT_Utf8_info 方法也有属性表 |
access_flag 标志
基本也同方法的修饰符
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_BRIDGE | 0x0040 | 方法是否为 编译器为了字节码兼容自动生成的bridge方法 |
ACC_VARARGS | 0x0080 | 方法是否为变长参数 |
ACC_NATIVE | 0x0100 | 方法是否为native 本地方法 |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract 无实现代码 |
ACC_STRICT | 0x0800 | 方法是否为strictfp 使用FP-strict浮点模式 |
ACC_SYNTHETIC | 0x1000 | 方法是否为编译器自动产生而不是由源代码编译而来 |
volatile关键字和transient关键字不能修饰方法,所以自然也没有这些个标志
而synchronized native strictfp 和 abstract 关键字可以修饰方法 所以相对于字段中的标志新增了对应的标志
类似字段,有些方法修饰符标志也是互斥的
一个方法只能设置ACC_PUBLIC ACC_PRIVATE ACC_PROTECTED 中的一种
接口方法可以设置 除了ACC_PROTECTED ACC_FINAL ACC_SYNCHRONIZED ACC_NATIVE 以外的,以外的
版本号小于52 每个方法必须设置 ACC_PUBLIC ACC_ABSTRACT
大于等于52 每个方法必须设置ACC_PUBLIC 或者 ACC_PRIVATE 中的一个
ps: 52 可以理解为jdk1.8
如果设置了ACC_ABSTRACT 不能再设置 ACC_FINAL ACC_NATIVE ACC_PRIVATE ACC_STATIC ACC_STRICT ACC_SYNCHRONIZED
实例初始化方法只能被ACC_PUBLIC ACC_PROTECTED ACC_PRIVATE 其中之一修饰
还可以设置 ACC_STRICT ACC_VARARGS ACC_SYNTHETIC 其他的都不能再设置
类或者接口的初始化方法 由虚拟机自动调用 除了ACC_STRICT以外,其它标志全部都会被忽略
通过类 常量池 字段 方法的结构,已经塑造完成了 class文件的基本概念
他们是class文件的基础骨架
骨架之上还有其他很多的附属信息以及比如运行时需要的额外的信息
这些信息大多数不能归结于一大类,逻辑架构上可能比较散乱,也可以理解为杂项信息
这些杂项就都是属性表的范畴
不过Code属性比较特殊,他其实也算作是一个骨架部分,或者说一个重要"器官" 他是作为方法中的代码编译后的字节码形式存在的
只不过因为逻辑上 方法内的代码字节码指令显然是归属于某个方法的,所以Code作为属性表也可以理解
class文件的ClassFile结构
字段的field_info 结构
方法的method_info结构
另外还有Code属性
以上四类都包含属性结构信息
所有属性表的梗概结构为
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
attribute_name_index 表示属性的名字索引 指向 CONSTANT_Utf8_info
attribute_length就是属性的长度
info[attribute_length] 是属性的具体数据信息
所有的属性按照用途,可以划分为三类
1.对于JVM 正确解读class文件起关键作用的5个属性 • ConstantValue • Code • StackMapTable • Exceptions • BootstrapMethods |
2.对JavaSE 平台类库正确解读class文件起关键作用的12个属性 • InnerClasses • EnclosingMethod • Synthetic • Signature • RuntimeVisibleAnnotations • RuntimeInvisibleAnnotations • RuntimeVisibleParameterAnnotations • RuntimeInvisibleParameterAnnotations • RuntimeVisibleTypeAnnotations • RuntimeInvisibleTypeAnnotations • AnnotationDefault • MethodParameters |
对JVM或者JavaSE平台类库能够正确解读class文件 虽然不起关键作用,但是却可以作为实用工具来使用的6个属性 • SourceFile • SourceDebugExtension • LineNumberTable • LocalVariableTable • LocalVariableTypeTable • Deprecated |
我们已经知道 属性出现于 classFile field_info method_info code 中
所有属性按照位置划分
属性 | 位置 | 备注 | 首次出现版本号 |
SourceFile |
ClassFile | 表示class文件的源文件名称 类独有属性 |
45.3 |
InnerClasses |
ClassFile | 内部类相关信息 类独有属性 |
45.3 |
EnclosingMethod |
ClassFile | class为局部类或者匿名类才具有 类独有属性 |
49.0 |
SourceDebugExtension |
ClassFile | 可选/保存扩展调试信息/最多一个 类独有属性 |
49.0 |
BootstrapMethods | ClassFile | 与 invokedynamic指令 常量池中CONSTANT_InvokeDynamic_info 相关 类独有属性 |
51.0 |
ConstantValue | field_info | fina修饰的字段的常量值 字段独有属性 |
45.3 |
Code | method_info | java程序方法体中的代码经过javac编译器处理后 最终变为字节码指令存储在Code属性内 Code属性出现在方法表的属性集合中 抽象类和接口不存在code属性 包含了方法的java虚拟机指令及相关辅助信息 方法独有属性 |
45.3 |
Exceptions | method_info | 方法可能抛出的已检查异常列表 方法独有属性 |
45.3 |
RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations |
method_info | 形参上的运行时的注解信息类型 分为可见和不可见两种类型 方法独有属性 |
49.0 |
AnnotationDefault | method_info | method_info表示注解类型中的元素时 记录这个元素的默认值 方法独有属性 |
49.0 |
MethodParameters | method_info | 形参相关信息,比如参数名称 方法独有属性 |
52.0 |
Synthetic | classFile field_info method_info |
Synthetic 标志编译器生成 类 字段 方法都可能由编译器生成 所以三种都有此属性 |
45.3 |
Deprecated | classFile field_info method_info |
语义同@Deprecated 显然可以标注在类/接口/字段/方法上 所以三种都有此属性 |
45.3 |
Signature | classFile field_info method_info |
泛型信息 类接口 字段 方法 都有可能有类型参数 所以三种都有此属性 |
49.0 |
RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations |
classFile field_info method_info |
类 方法 字段上 运行时注解的可见性 分为可见不可见两种类型 三种都有此属性 |
49.0 |
LineNumberTable | Code | 调试用信息 用于调试器确定源文件中给定行号所表示的内容,对应于虚拟机中code[]数组中的哪一部分 也就是行号与字节码指令的对应关系 |
45.3 |
LocalVariableTable | Code | 调试用信息 调试器执行方法过程中可以用它来确定某个局部变量的值 |
45.3 |
LocalVariableTypeTable | Code | 调试用信息 调试器执行方法过程中可以用它来确定某个局部变量的值 |
49.0 |
StackMapTable | Code | 虚拟机类型检查验证使用信息 | 50.0 |
RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations |
classFile field_info method_info |
类/方法/字段声明所使用的类型上面的运行时注解可见性 分为可见/不可见两种 三种都有此属性 |
52.0 |
变换一种组织形式
我们以位置为纲 可以很清晰的看到四个位置处都有那些信息
classFile | SourceFile InnerClasses EnclosingMethod SourceDebugExtension BootstrapMethods Synthetic Deprecated Signature RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations |
field_info | ConstantValue Synthetic Deprecated Signature RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations |
method_info | Code Exceptions RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations AnnotationDefault MethodParameters Synthetic Deprecated Signature RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations |
Code | LineNumberTable LocalVariableTable LocalVariableTypeTable StackMapTable |
属性表中的attribute_name_index 都是对应的属性名称 指向CONSTANT_Utf8_info 比如Code属性值为Code ConstantValue属性为ConstantValue
ConstantValue 属性
通知虚拟机为静态变量赋值
只有被static关键字修饰的变量才可以使用这个属性 也就是只有类变量才可以使用
非static类型的变量也就是实例变量的赋值在构造方法中
类变量可以再
目前编译器的做法是 如果同时使用final和static来修饰,也就是常量了
如果变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue
如果没有final 或者并非基本类型或者字符串 选择在
ConstantValue_attribute { u2 attribute_name_index; u4 attribute_length; u2 constantvalue_index; } |
Code属性
Code属性是方法体中的代码经过编译处理后,最终的字节码指令
既然是方法体的内容,如果没有方法体自然没有code属性 比如 接口或者抽象类中就不存在Code属性 native也不存在
Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count]; } |
attribute_name_index 指向CONSTANT_Utf8_info 常量值 固定为"Code" 表示属性的名称
attribute_length 属性值长度 属性表前面6个字节 u2 + u4 再加上attribute_length 就是整个表的长度了
max_stack 操作数栈的最大深度 方法执行任意时刻不会超过这个值,根据值来分配栈帧
ps: 虚拟机栈是线程私有的,每创建一个线程,虚拟机就会为这个线程创建一个虚拟机栈 虚拟机栈表示Java方法执行的内存模型,每调用一个方法就会为每个方法生成一个栈帧(Stack Frame),用来存储局部变量表、操作数栈、动态链接、方法出口等信息。 每个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程。 虚拟机栈的生命周期和线程是相同的 |
max_locals 局部变量表所需要的空间单位个数,可以理解为座位 这个位置叫做 Slot 可以查看javap后的信息
long double占用两个单位 其他类型一个单位
并不是有多少局部变量就是求和计算,因为变量有作用域的生命周期,所以空间可以复用
编译器会自动分配 ,并且计算出来这个值, 可以理解为 任意时刻最大的所需要空间个数
code_length 和code 才是真正的存储字节码的,虽然我们说code属性就是存储编译后的字节码
code_length指明了字节码指令的长度 字节码指令为u1
读取一个指令后虚拟机就可以知道这个字节码的含义以及这条指令后面是否还有参数
以及参数如何理解
exception_table 异常表表示的是可能出现的代码执行路径
表示如果字节码start_pc行 到 end_pc行,包含头不包含为 也就是不包含end_pc
出现了类型为catch_type 或者他的子类的异常 catch_type 指向一个CONSTANT_Class_info
转到handler_pc中处理
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
上面的代码除了正常执行以外
如果try出现Exception或者子类的异常 转到catch
如果出现不属于Exception或者子类的异常 转到finally
如果catch中出现任何异常,转到finally
StackMapTable属性
用于虚拟机类型检查的验证阶段
是为了一种新的类型检查验证器而设置的,新的验证器在编译阶段将一系列的验证类型直接记录在class文件中,
通过检查这些验证类型代替了类型推导过程
Code属性表里最多可以包含一个StackMapTable
StackMapTable包含0个或者多个栈帧映射
用于表示 执行到该字节码时局部变量表和操作数栈的验证类型
类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束
版本号大于等于50的class文件中如果方法的code属性中没有附带StackMapTable属性,意味着他有一个 隐式的栈帧映射属性
隐式的栈映射属性的作用等同于number_of_entries 值为0的StackMapTable
StackMapTable_attribute { u2 attribute_name_index; u4 attribute_length; u2 number_of_entries; stack_map_frame entries[number_of_entries]; } |
number_of_entries给出来的是stack_map_frame的个数
union stack_map_frame { same_frame; same_locals_1_stack_item_frame; same_locals_1_stack_item_frame_extended; chop_frame; same_frame_extended; append_frame; full_frame; } |
Exceptions属性
不是Code中的exception_table 注意区分
列举出方法中可能抛出的已检查的异常
也就是方法声明throws后面的内容
Exceptions_attribute { u2 attribute_name_index; u4 attribute_length; u2 number_of_exceptions; u2 exception_index_table[number_of_exceptions]; } |
attribute_name_index 指向名字
attribute_length表示长度
number_of_exceptions 表示个数
exception_index_table 指向常量池中的CONSTANT_Class_info 表示异常类型
BootstrapMethods 属性
保存invokedynamic 指令引用的引导方法限定符
如果某个classFile文件中常量池中至少有一个CONSTANT_InvokeDynamic_info
那么就必须包含且只能包含一个BootstrapMethods
与invokedynamic指令和java.lang.invoke包关系密切
BootstrapMethods_attribute { u2 attribute_name_index; u4 attribute_length; u2 num_bootstrap_methods; { u2 bootstrap_method_ref; u2 num_bootstrap_arguments; u2 bootstrap_arguments[num_bootstrap_arguments]; } bootstrap_methods[num_bootstrap_methods]; } |
InnerClasses 属性
记录内部类与宿主类之间的关联
如果一个类内部定义了内部类,编译器就会为以及他所包含的内部类生成InnerClasses属性
InnerClasses_attribute { u2 attribute_name_index; u4 attribute_length; u2 number_of_classes; { u2 inner_class_info_index; u2 outer_class_info_index; u2 inner_name_index; u2 inner_class_access_flags; } classes[number_of_classes]; } |
inner_class_info_index outer_class_info_index 都是指向常量池中CONSTANT_Utf8_info
分别代表内部类和宿主类的内部引用
inner_name_index指向常量池中CONSTANT_Utf8_info 表示内部类的名称 匿名内部类 为0
inner_class_access_flags 内部类的访问标志
ACC_PUBLIC | 0x0001 | 内部类是否为public |
ACC_PRIVATE | 0x0002 | 内部类是否为private |
ACC_PROTECTED | 0x0004 | 内部类是否为protected |
ACC_STATIC | 0x0008 | 内部类是否为static |
ACC_FINAL | 0x0010 | 内部类是否为final |
ACC_INTERFACE | 0x0200 | 内部类是否为interface |
ACC_ABSTRACT | 0x0400 | 内部类是否为abstract |
ACC_SYNTHETIC | 0x1000 | 内部类是否为编译器自动生成 |
ACC_ANNOTATION | 0x2000 | 内部类是否为注解 |
ACC_ENUM | 0x4000 | 内部类是否为枚举 |
Synthetic 属性
标志是否有编译器自动生成 没有具体的值 只有存在和不存在的说法
Synthetic_attribute { u2 attribute_name_index; u4 attribute_length; } |
attribute_name_index 指向CONSTANT_Utf8_info 表示 synthetic
attribute_length 固定为0 也就是么有值
Signature 属性
可选的属性 1.5之后 任何 类 接口 初始化方法 或者成员 的泛型签名如果包含了类型变量 或者 参数化类型
那么signature 属性记录泛型签名信息
之所以需要是因为泛型擦除机制
反射机制获取泛型类型 依赖数据就是这个属性
Signature_attribute { u2 attribute_name_index; u4 attribute_length; u2 signature_index; } |
EnclosingMethod 属性
位于classFile结果的属性 当且仅当class为局部类和匿名内部类时 才具有这个属性
class_index 表示包含当前类的最内层类
method_index表示当前类是否在某个方法或者构造器中,如果不是 值为0
EnclosingMethod_attribute { u2 attribute_name_index; u4 attribute_length; u2 class_index; u2 method_index; } |
RuntimeVisibleAnnotations 属性 RuntimeInvisibleAnnotations 属性
添加在类声明 字段声明 方法声明上面的注解 在运行时的可见情况
Visible 可见 Invisible不可见
ClassFile field_info method_info中最多只能有一个RuntimeVisibleAnnotations 或者 RuntimeInvisibleAnnotations
RuntimeVisibleAnnotations 和 RuntimeInvisibleAnnotations 基本一致, 但是 RuntimeInvisibleAnnotations 标志的不能被反射API访问
除非虚拟机通过与实现相关的特殊方式保留这些注解,否则,虚拟机将忽略Invisible的注解
num_annotations表示注解的数量
annotations 每个元素都表示一个注解
RuntimeVisibleAnnotations_attribute { u2 attribute_name_index; u4 attribute_length; u2 num_annotations; annotation annotations[num_annotations]; } |
RuntimeInvisibleAnnotations_attribute { u2 attribute_name_index; u4 attribute_length; u2 num_annotations; annotation annotations[num_annotations]; } |
annotation { u2 type_index; u2 num_element_value_pairs; { u2 element_name_index; element_value value; } element_value_pairs[num_element_value_pairs]; }
type_index 用来表示一个字段描述符,字段描述符表示一个注解类型 和当前annotation 结构所表示的注解一致 num_element_value_pairs 表示注解中的键值对 (注解中的参数都是键值对) 的个数 element_value_pairs 代表真正的键值对 它包括 element_name_index表示键 element_value 表示值
element_value { u1 tag; union { u2 const_value_index; { u2 type_name_index; u2 const_name_index; } enum_const_value; u2 class_info_index; annotation annotation_value; { u2 num_values; element_value values[num_values]; } array_value; } value; }
element_value 表示一个联合体 tag 使用u1 来表示键值对中的值是什么类型 也就是决定了键值对中 值的格式与value 中的哪一项相符合 联合体总共有五种 const_value_index enum_const_value class_info_index annotation_value array_value |
tag 值表示的类型
B | byte | const_value_index | CONSTANT_Integer |
C | char | const_value_index | CONSTANT_Integer |
D | double | const_value_index | CONSTANT_Double |
F | float | const_value_index | CONSTANT_Float |
I | int | const_value_index | CONSTANT_Integer |
J | long | const_value_index | CONSTANT_Long |
S | short | const_value_index | CONSTANT_Integer |
Z | boolean | const_value_index | CONSTANT_Integer |
s | String | const_value_index | CONSTANT_Utf8 |
e | Enum类型 | enum_const_value | 不适用 |
c | Class | class_info_index | 不适用 |
@ | Annotation类型 | annotation_value | 不适用 |
[ | Array 类型 | array_value | 不适用 |
const_value_index 表示原始类型的常量值 或者String类型的字面量 |
enum_const_value 表示一个枚举常量 type_name_index 指向CONSTANT_Utf8_info 枚举常量类型的二进制名称的内部形式 const_name_index 指向CONSTANT_Utf8_info 枚举常量的简单名称 |
class_info_index 表示类字面量 CONSTANT_Utf8_info 用于表示返回描述符 返回描述符给出了与该element_value结构所表示的类字面量相对应的类型 如果类字面量是C. class,且C是类、接口或数组类型的名字,那么对应的类型就是C。常量池中的返回描述符会是ObjectType 或者ArrayType 如果类字面量是p. class,且p是原始类型的名称,那么对应的类型就是p 常量池中的返回描述符会是一个BaseType 如果类字面量是void. class,那么对应的类型就是void。常量池中的返回描述符会是V. 比如Object.class 对应于类型Object 所以常量池中就是Ljava/lang/Object; 而 int.class对应于类型int 常量池中就是I(大写的i ) |
annotation_value 表示键值对中里面的值本身又是一个注解 |
array_value 表示键值对的 值 是一个数组 num_values 给出了当前element_value结构所表示的数组的成员数量 values 每个成员对应了当前element_value 结构所表示的数组中的一个元素 |
RuntimeVisibleTypeAnnotations 属性 RuntimeInvisibleTypeAnnotations 属性
classFile field_info method_info 或者code属性中都有
比如对于某个类声明implements后面的类型所加的注解 记录在classFile结构体的这个属性里
比如某个字段声明中的类型 所加的全部注解记录在字段的对应属性里
记录了标注在对应类声明 字段声明 或者方法声明所使用的类型上面的注解 在运行时的可见情况
分为可见和不可见两种
也记录了标注在对应方法体重某个表达式所使用的类型上面的运行时可见注解
此外还记录了标注在泛型类 接口 方法 以及构造器的类型参数声明上面的注解
Java虚拟机必须使这些注解可供取用
最多只能有一个
num_annotations 表示注解个数
annotations 表示每一个注解 类型为type_annotation
RuntimeVisibleTypeAnnotations_attribute { u2 attribute_name_index; u4 attribute_length; u2 num_annotations; type_annotation annotations[num_annotations]; } |
RuntimeInvisibleTypeAnnotations_attribute { u2 attribute_name_index; u4 attribute_length; u2 num_annotations; type_annotation annotations[num_annotations]; } |
type_annotation { u1 target_type; union { type_parameter_target; supertype_target; type_parameter_bound_target; empty_target; method_formal_parameter_target; throws_target; localvar_target; catch_target; offset_target; type_argument_target; } target_info; type_path target_path; u2 type_index; u2 num_element_value_pairs; { u2 element_name_index; element_value value; } element_value_pairs[num_element_value_pairs]; } |
前三项 target_type target_info 以及 target_path 指出了带注解的类型所在的精确位置 target表示那个类型
后面type_index num_element_value_pairs element_value_pairs指出了注解本身的类型以及键值对
RuntimeVisibleParameterAnnotations 属性 RuntimeInvisibleParameterAnnotations 属性
保存标注在对应方法的形式参数声明上面的注解的运行时可见状态
分为可见和不可见两种
num_parameters 形参个数
parameter_annotations 每个元素表示一个形式参数的运行时注解 第 n项,表示方法描述符中的第 n 个形式参数
RuntimeVisibleParameterAnnotations_attribute { u2 attribute_name_index; u4 attribute_length; u1 num_parameters; { u2 num_annotations; annotation annotations[num_annotations]; } parameter_annotations[num_parameters]; } |
RuntimeInvisibleParameterAnnotations_attribute { u2 attribute_name_index; u4 attribute_length; u1 num_parameters; { u2 num_annotations; annotation annotations[num_annotations]; } parameter_annotations[num_parameters]; } |
AnnotationDefault 属性
如果一个method_info 是用来表述注解类型中的元素的
该结构体的属性表中最多只能有一个AnnotationDefault
AnnotationDefault属性记录了由method_info 结构所表示的那个元素的默认值
default_value 表示由AnnotationDefault属性外围的method_info结构所描述的那个注解类型元素的默认值
说白了这个结构有用的才是default_value
AnnotationDefault_attribute { u2 attribute_name_index; u4 attribute_length; element_value default_value; } |
MethodParameters 属性
形参相关的一些信息 比如参数名称
parameters_count 表示本属性外围method_info 结构里面的descriptor_index所引用的那个方法描述符中,有多少个参数描述符
parameters 表示实际的参数
name_index 要么是0要么指向CONSTANT_Utf8_info 表示一个有效的非限定名 用来指代某个形式参数
access_flags ACC_FINAL 0x0010 形参为final
ACC_SYNTHETIC 0x1000 形参没有显式或者隐式的在源代码中声明,由编译器生成
ACC_MANDATED 0x8000 形参是隐式声明,也就是编程语言规范对所有的编译器的要求必须生成
MethodParameters_attribute { u2 attribute_name_index; u4 attribute_length; u1 parameters_count; { u2 name_index; u2 access_flags; } parameters[parameters_count]; } |
sourceFile 属性
class文件的源文件名,属性是可选的可以关闭
但是一旦关闭,当抛出异常时 不会显示出错代码所归属的文件名称
SourceFile_attribute { u2 attribute_name_index; u4 attribute_length; u2 sourcefile_index; } |
sourcefile_index 指向的是常量池中的CONSTANT_Utf8_info 表示源文件的文件名
SourceDebugExtension 属性
一个classFile只能包含一个属性
debug_extension 用于保存扩展调试信息 扩展调试信息对于java虚拟机来说没有实际的语义
扩展信息使用改进版的UTF8 编码的字符串 也可以说这个就算是另一种格式的用于表示字符串的结构
不过他比String类的实例所能表示的字符串更长
SourceDebugExtension_attribute { u2 attribute_name_index; u4 attribute_length; u1 debug_extension[attribute_length]; } |
LineNumberTable 属性
Code属性的属性表中
源文件中给定的行号表示的内容对应字节码指令的行号(偏移量)之间的关系
并不是运行时必须信息,但是会默认生成到Class文件中
可以通过参数设置不生成,但是程序运行产生的最主要影响就是当抛出异常时,堆栈中不会显示出错的行号
调试时也无法设置断点
LineNumberTable_attribute { u2 attribute_name_index; u4 attribute_length; u2 line_number_table_length; { u2 start_pc; u2 line_number; } line_number_table[line_number_table_length]; }
|
LocalVariableTable 属性
用于描述栈帧中的局部变量表中的变量与java源代码中定义的变量之间的关系
也可以不生成,但是可能会导致别人引用方法时,参数名称丢失
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 属性
与LocalVariableTable类似,就是descriptor_index 被替换为了signature_index
主要针对泛型场景
非泛型签名与描述符基本一致,引入泛型后实际的泛型信息会被擦除 描述符就不足够准确了
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; u2 index; } local_variable_type_table[local_variable_type_table_length]; } |
Deprecated 属性
语义同语法中的@Deprecated 形式类似Synthetic 标志属性 有和没有的区别
attribute_length 固定为0
Deprecated_attribute { u2 attribute_name_index; u4 attribute_length; } |
class文件的最详细的介绍
自然是官方文档
https://docs.oracle.com/javase/specs/index.html
包含多个版本的JDK
以及两种格式
另外也有中文版