Class类文件结构
Class文件是一组以8位字节为基础单位的二进制流;
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表;
无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数, 无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成字符串值;
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾.表用于描述有层次关系的复合结构的数据, 整个Class文件本质上就是一张表;
Class文件中数据项目严格按照顺序紧凑地排列,没有任何分隔符号,所以所有的数据项,无论是顺序还是数量,甚至于数据存储的字节序这样的细节,都是被严格限定的.哪个字节代表什么含义,长度是什么,先后顺序如何,都不允许改变;
类型 | 名称 | 数量 |
---|---|---|
u4 | magic(魔数) | 1 |
u2 | minor_version(次版本号) | 1 |
u2 | major_version(主版本号) | 1 |
u2 | constant_pool_count(常量池数量) | 1 |
cp_info | constant_pool(常量池) | constant_pool_count |
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 | fileds(字段表集合) | fields_count |
u2 | methods_count(方法计数) | 1 |
method_info | methods(方发表集合) | methods_count |
u2 | attributes_count(属性计数) | 1 |
attribute_info | attributes(属性表集合) | attributes_count |
魔数与Class文件的版本
使用十六进制编辑器打开Class文件
魔数(magic):每个Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件.它的的值固定为0xCAFEBABE;
版本(minor_version,major_version):紧接着魔数的4个字节存储的是Class文件的版本号; 第5和第6个字节是次版本号, 第7和第8是主版本号.图示中的值为0x00000034, 换算十进制是52,对应的是JDK1.8;
常量池
常量池(constant_pool):可以理解为Class文件之中的资源仓库, 他是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目;
常量池入口(constant_pool_count):紧接着主次版本号之后的就是常量池入口(第9个字节开始), 常量池的入口需要放置一项u2类型(占用2个字节)的数据,代表常量池容量的计数值(constant_pool_count),此计数值是从1开始,因为第0项常量空出来是有特殊考虑的, 目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达"不引用任何一个常量池项目"的含义; 分析图中的常量池入口值0x004E,换算十进制为78,则说明此类的常量池中有77个常量;
常量池主要存放两大类常量: 字面量和符号引用.字面量比较接近于JAVA语言层面的常量概念, 如文本字符串,声明为final的常量值等.而符号引用则属于编译原理方面的概念,包括类和接口的全限定名,字段的名称和描述符,方法的名称和描述符;
标志位(tags):常量池中每一项常量都是一个表,设计中定义了十四种不同表结构,用于代表不同的常量类型;这些表结构都有一个共同特点,就是表开始的第一位是一个u1类型的标志位(tags), 用于代表当前这个常量属于哪种常量类型;
分析图示中第一个常量(第11个字节开始)的Tags, 值为0x0A,那么代表此类的第一个常量是CONSTANT_Methodref_info类型;根据表结构分析, 接下来有两个u2类型的index的值分别指向其他索引, 0x0013和0x002E分别指向索引为19, 46的常量池项;具体19,46的常量池是什么值需要往下继续分析;
常量类型 | 结构项目-项目类型-描述 | |||||||||
---|---|---|---|---|---|---|---|---|---|---|
CONSTANT_Utf8_info |
| |||||||||
CONSTANT_Integer_info |
| |||||||||
CONSTANT_Float_info |
| |||||||||
CONSTANT_Long_info |
| |||||||||
CONSTANT_Double_info |
| |||||||||
CONSTANT_Class_info |
| |||||||||
CONSTANT_String_info |
| |||||||||
CONSTANT_Fieldref_info |
| |||||||||
CONSTANT_Methodref_info |
| |||||||||
CONSTANT_Interfacemethodref_info |
| |||||||||
CONSTANT_NameAndType_info |
| |||||||||
CONSTANT_MethodHandle_info |
| |||||||||
CONSTANT_MethodType_info |
| |||||||||
CONSTANT_InvokeDynamic_info |
|
JDK的bin目录中,存在一个专门分析Class文件字节码的工具:javap, 使用命令javap -verbose xxx.class 可以查看字节码内容;
下图可以看出之前对常量池的分析是正确的, 此Class文件存在77个常量, 其中第一个常量为CONSTANT_Methodref_info类型,它指向索引值为19和46的常量;
访问标志
访问标志(access_flags):用于识别类或者接口层次的访问信息, 包括这个Class文件是类还是接口,是否被定义为public类型,是否定义为abstract类型;如果是类是否被声明为final等;
访问标志是一个u2类型的数据项,常量池结束后,紧接着的两个字节就代表着访问标志的值;
官方定义中访问标志一共有十六种标志位可以使用, 当前只定义了其中8个, 没有使用到的标志位要求一律为0.将所有使用到的标志位做或位运算得到的最后的值即为访问标志的值;
标志名称 | 标志值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final, 只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDK1.02发生过改变,为了区别这条指令使用哪种语意,JDK1.02之后编译出来的类的这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志为真,其他类为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非有用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
已知当前Class文件的类StringTest.class是一个普通的类,那么应该对应的是ACC_PUBLIC和ACC_SUPER两个标志, 0x0001|0x0020=0x0021, 与字节码文件中显示的一致;
再看另一个Class文件, 已知此文件是一个普通的枚举类型, 枚举类型的实现就是一个final的类, 那么它的标志应该是ACC_PUBLIC、ACC_SUPER、ACC_FINAL、ACC_ENUM,即0x0001|0x0010|0x0020|0x4000=0x4031, 与字节码中文件显示的一致;
类索引,父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,接口索引集合(interfaces)是一组u2类型的数据集合,Class文件中由这三项数据来确定这个类的继承关系.
类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量, 通过CONSTANT_CLASS_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中全限定名字符串.
对于接口索引集合,入口第一项是一个u2类型的数据,作为接口计数器(interfaces_count),表示索引表的容量.如果该类没有实现任何接口,则该计数器的值为0,后面接口的索引表不再占用任何字节.
依旧分析之前的Class文件, 如下图, 在访问标志之后就是类索引 0x000E=14, 指向索引为14的常量, 根据javap -v命令可以看到索引为14的常量是CONSTANT_CLASS_info类型, 此常量指向索引为62的常量, 而索引为62的常量是CONSTANT_Utf8_info类型常量, 它的值为com/bryan/study/baseclass/StringTest, 那么表明this_class是com/bryan/study/baseclass/StringTest; 同理分析父类索引, 0x0013=19, 父类索引指向索引为19的常量,最终指向索引为66的常量java/lang/Object, 那么当前类的父类就是java/lang/Object;分析接口索引集合, 当前文件中接口索引集合计数器为0, 那么表示当前类没有实现任何接口;
字段表集合
字段表数量(field_count):紧跟接口集合后面的一个u2类型的数据,用于表示类的字段数量;
字段表(field_info): 用于描述接口或者类中声明的变量.
字段包括类变量和实例变量,但不包括方法内的局部变量.字段表包括字段的各种修饰符,使用u2类型的标志位来表示.字段的名称以及数据类型使用一个指向常量池索引的u2类型的值来表示; 字段表的整体结构包括 access_flags,name_index,descriptor_index,attributes_count,attributes;
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags(访问标志) | 1 |
u2 | name_index(名称在常量池的索引) | 1 |
u2 | decsriptor_index(描述在常量池的索引) | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段层级访问标志(access_flags):它与类中的access_flags项目是非常类似的,都是一个u2的类型数据,用于表示字段的各种修饰符;
标志名称 | 标志值 | 含义 |
---|---|---|
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 |
简单名(name_index)和描述符(descriptor_index):跟随着access_flags的两个索引值, 它们都是对常量池的引用; 分别代表着字段的简单名称和字段的描述符. 简单名称是指没有类型和参数修饰的方法或者字段名称;描述符的作用是用来描述字段的数据类型或者方法参数列表的类型和返回值.
标识字符 | 含义 | 标识字符 | 含义 |
---|---|---|---|
B | byte | J | long |
C | char | S | short |
D | double | Z | boolean |
F | float | V | void |
I | int | L | 引用类型 |
[ | 数组 |
分析StringTest.class文件: 可以看到field_count是0x0005,说明此类有五个字段属性; 紧接着第一个字段的access_flags为0x0000,说明此字段没有任何修饰符;接着是name_index为0x0014=20指向常量池索引为20的常量,查看常量池索引为20的常量是一个CONSTANT_Utf8_info类型的常量,值为I,说明此字段名称为I; 接着是descriptor_index为0x0015=21指向常量池索引为21的常量,查看常量池此常量是一个CONSTANT_Utf8_info类型的常量, 值为Ljava/lang/Integer,说明此字段的类型是引用类型的Integer,所以可以推断出第一个字段的整体是: Integer I;紧跟着是attributes_count为0x0000,表示没有属性数量;
方法表集合
methods_count(方法计数):一个u2类型的数据,用于表示类有多少个方法;
methods(方法表集合):方法表集合,用于描述类中所有的方法;
类型 | 名称 | 数量 | 类型 | 名称 | 数量 |
---|---|---|---|---|---|
u2 | access_flags | 1 | u2 | attributes_count | 1 |
u2 | name_index | 1 | attribute_info | attributes | attributes_count |
u2 | descriptor_indwx | 1 |
方法层级访问标识(access_flags);attributes_count:它与类中的access_flags项目是非常类似的,都是一个u2的类型数据,用于表示方法的各种修饰符;
标识名称 | 标识值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否public |
ACC_PRIVATE | 0x0002 | 是否为private |
ACC_PROTECTED | 0x0004 | 是否protected |
ACC_FINAL | 0x0010 | 是否final |
ACC_SYNCHRONIZED | 0x0020 | 是否synchronized |
ACC_BRIDGE | 0x0040 | 是否是有编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 是否接受不定参数 |
ACC_NATIVE | 0x0100 | 是否为native |
ACC_ABSTRACT | 0x0400 | 是否为abstract |
ACC_STRICTFP | 0x0800 | 是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 是否有编译器自动产生的 |
ACC_STATIC | 0x0008 | 是否static |
简单名(name_index):与字段表中类似的去分析即可;
描述符(descriptor_index):描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号之内.例如java.lang.String.toString()方法描述为()Ljava/lang/String;方法int indexOf(char[]source,int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述为([CII[CIII)I.
属性表集合
在Class文件中, 字段表,方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息;与Class文件中其他的数据项目要求严格的瞬息,长度和内容不同, 属性表集合的限制稍微宽松一些,不在要求各个属性表具有严格顺序, 并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息;
属性名 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类,方法表,字段表 | 被声明为deprecated的方法和字段 |
Exception | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法; |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查验证器检查和处理目标方法的局部变量和操作数栈所需的类型是否匹配 |
Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名,在Java语言中, 任何类,接口,初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则Signature属性会为它记录泛型签名信息.由于Java的泛型采用擦除法实现, 在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的信息 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | |
Synthetic | 类,方法表,字段表 | 表示方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | 它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotions | 类,方法表, 字段表 | 为动态注解提供支持.,指明哪些注解是运行时可见的 |
RuntimeInvisibleAnnotation | 类,方法表, 字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisableParameterAnnotations | 方法表 | 作用对象为方法参数, 用于指明哪些注解是运行时可见的 |
RuntimeInVisableParameterAnnotations | 方法表 | 作用对象为方法参数, 用于指明哪些注解是运行时不可见的 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokedynamic指令用用的引导方法限定符 |
符合规则的属性表应该满足以下结构, 才能够被JVM识别, 否则在运行时虚拟机会忽略掉不认识的属性;
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |