文章目录
1. Class文件结构
Class文件是一组以8个字节为基础单位的二进制流,紧凑排列无分隔符。当存在需要占用8个字节以上空间的数据项时,则按照高位再迁的方式,分割为8个字节进行存储。
Class文件采用类似于C语言结构体的伪数据结构来存储数据,它有两种数据类型:无符号数
,表
。
无符号数
:基本的数据类型,以u1、u2、u4、u8分别来代表1、2、4、8个字节的无符号数
,无符号数
可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值;
表
:由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表的命名惯以_info结尾。表
用于描述有层次关系的复合结构的数据。
Class文件结构 | |||
---|---|---|---|
类型 | 名称 | 数量 | 释义 |
u4 | magic | 1 | 魔数,用于判定该文件是否为一个能被JVM接受的Class文件,固定值为0xCAFEBABE |
u2 | minor_version | 1 | 副版本号 |
u2 | major_version | 1 | 主版本号,与副版本号共同组成Class文件格式版本号。 |
u2 | constant_pool_count | 1 | 常量池计数器 |
cp_info | constant_pool | constant_pool_count-1 | 常量池 |
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 | fields | fields_count | 字段表 |
u2 | methods_count | 1 | 方法表计数器 |
method_info | methods | methods_count | 方法表 |
u2 | attributes_count | 1 | 属性表计数器 |
attribute_info | attributes | attributes_count | 属性表 |
1.1 magic魔数与Class文件版本
魔数
:每个Class文件的前四个字节被称为Magic Number,值为0xCAFEBABE,用于确定这个文件是否是能被JVM接受的Class文件;
Minor Version
:第5个及第6个字节为次版本号;
Major Version
:第7和第8个字节是主版本号。
编写java文件进行编译:
public class Demo {
public static void main(String[] args) {
String numberString = "3";
}
}
使用javac
编译之后获取class文件,通过十六进制编辑器winhex打开Class文件我们可以看到如下信息:
同我们上面所述一致,前四个字节的十六进制表示为0xCAFEBABE,次版本号为第5-6字节,值为0x0000,主版本号为第7-8字节值为:0x0034,即十进制52,Java的版本号从45开始,从JDK1.1开始,每个JDK的大版本的主版本号在原基础加一(45+7=52,也就是JDK8现在的主版本号)。高版本JDK只可以向下兼容,即使文件格式并未发生变化,超过其版本号范围的JVM拒绝执行。
1.2 常量池
常量池
是Class文件结构中与其他项目关联最多的数据,也是Class文件空间最大的数据项之一,它还是Class文件中第一个出现的表类型数据项。
每个Class文件的第9和第10个字节代表常量池容量计数器constant_pool_count
:
常量池容量计数从1开始(第0项常量之所以空置出来,主要用于表达某些指向常量池的索引需要表达不引用任何一个常量池的项目的含义)。
如上图所示,常量池容量为0x0011即十进制17,说明常量池中有16项常量,索引取值范围1~16。
常量池中主要存放量大类常量:
字面量
(Literal):例如文本字符串、被声明为final的常量值、基本数据类型值等。符号引用
(Symbolic References):主要包含以下几类- 被模块导出或开放的包(Package);
- 类和接口的全限定名(Fully Qualified Name);
- 字段的名称和描述符(Descriptor);
- 方法的名称和描述符;
- 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic);
- 动态调用点和动态常量(Dynamically-Computed Call Side、Dynamically-Computed Constant)
1.2.1 常量池的项目类型
常量池中每一项常量都是一个表,常量池的项目类型如下:
常量池的项目类型 | |||
---|---|---|---|
常量 | 项目 | 类型 | 描述 |
CONSTANT_Utf8_info(UTF-8编码字符串表) | tag | u1 | 1 |
length | u2 | UTF-8编码的字符串占用的字节数 | |
bytes | u1 | 长度为length的UTF-8编码的字符串 | |
CONSTANT_Integer_info(整型常量表) | tag | u1 | 3 |
bytes | u4 | 按照高位在前存储的int值 | |
CONSTANT_Float_info(浮点型常量表) | tag | u1 | 4 |
bytes | u4 | 按照高位在前存储的float值 | |
CONSTANT_Long_info(长整型常量表) | tag | u1 | 5 |
bytes | u8 | 按照高位在前存储的long值 | |
CONSTANT_Double_info(双精度浮点型常量表) | tag | u1 | 6 |
bytes | u8 | 按照高位在前存储的double值 | |
CONSTANT_Class_info(类或接口引用表) | tag | u1 | 7 |
index | u2 | 指向全限定名常量项的索引 | |
CONSTANT_String_info(字符串常量表) | tag | u1 | 8 |
index | u2 | 指向字符串字面量的索引 | |
CONSTANT_Fieldref_info(字段引用表) | tag | u1 | 9 |
class_index | u2 | 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项 | |
name_type_index | u2 | 指向声明字段的类或接口描述符CONSTANT_NameAndType的索引项 | |
CONSTANT_Methodref_info(方法引用表) | tag | u1 | 10 |
class_index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 | |
name_type_index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引项 | |
CONSTANT_InterfaceMethodref_info(接口方法引用表) | tag | u1 | 11 |
class_index | u2 | 指向声明方法的接口描述符CONSTANT_Class_info | |
name_type_index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引项 | |
CONSTANT_NameAndType_info(字段或方法名称类型表) | tag | u1 | 12 |
class_index | u2 | 指向该字段或方法名称常量项的索引 | |
name_type_index | u2 | 指向该字段或方法描述符常量项的索引 | |
CONSTANT_MethodHandle_info(方法句柄表) | tag | u1 | 15 |
reference_kind | u1 | 值必须在1-9的范围内,他决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为 | |
reference_index | u2 | 值必须是对常量池的有效索引 | |
CONSTANT_MethodType_info(方法类型表) | tag | u1 | 16 |
descriptor_index | u2 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符 | |
CONSTANT_InvokeDynamic_info(动态方法调用表) | tag | u1 | 18 |
bootstrap_method_attr_index | u2 | 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引 | |
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符 |
我们继续查看Class字节码文件:
0x0A转换为十进制是10,结合上面常量池类型表中tag等于10的是CONSTANT_Methodref_info
,表中tag字段后是数据类型为u2
的class_index
、name_type_index
,结合上面截图,分别对应0x0004(十进制4,指向常量池中第四个常量)和0x000D(十进制13,指向常量池第13个常量)。
通过winhex工具查看class文件的方式过于繁琐,我们还有一种较为直观的查看方式。
1.2.2 javap
F:\>javap -v Demo.class
Classfile /F:/Demo.class
Last modified 2020-12-25; size 267 bytes
MD5 checksum ed2eccdd0487686a954e6c2ea894d2a7
Compiled from "Demo.java"
public class Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object."<init>":()V
#2 = String #14 // 3
#3 = Class #15 // Demo
#4 = Class #16 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 SourceFile
#12 = Utf8 Demo.java
#13 = NameAndType #5:#6 // "<init>":()V
#14 = Utf8 3
#15 = Utf8 Demo
#16 = Utf8 java/lang/Object
{
public Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String 3
2: astore_1
3: return
LineNumberTable:
line 3: 0
line 4: 3
}
SourceFile: "Demo.java"
从如上打印信息可以看到,第一项**#1 = Methodref #4.#13**和我们计算的结果完全一致。
1.3 访问标志
在常量池接着的2个字节代表访问标志(access_flag),这个标志用于表示一些类或接口层次的访问信息。
主要包含以下几点:
- Class是类还是接口;
- 是否定义为public类型;
- 是定义为abstract类型;
- 若是类是否被声明为final;
- 其他。
访问标志 | ||
---|---|---|
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 是否为public |
ACC_FINAL | 0x0010 | 是否被声明为final |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的标志都为true,invokespecial(调用无须动态绑定的实例方法)只能调用:方法;private方法;super.method(),这三类方法的调用在对象在编译时就可以确定 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或抽象类,标志位为true |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识是注解 |
ACC_ENUM | 0x4000 | 标识是枚举 |
ACC_MODUEL | 0x8000 | 标识是模块 |
因Demo.java非final非abstract,public修饰,所以他的access_flags=0x0001+0x0020=0x0021
1.4 类索引、父类索引、接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三类数据来确认该类型的继承关系。类索引用于确定类的全限定名,父类索引引用于确定类的父类全限定名。Java不允许多重继承,父类索引只有一个,除java.lang.Object之外,所有的Java类都有父类,因此java类的父类索引都不为0(java.lang.Object的父类索引为0)。接口索引集合用来描述类实现了哪些接口,这些被实现的接口将按implements关键字后的接口顺序从左到右排列在接口索引集合中。
在访问标志后的3个u2类型的值分别是0x0003(类索引),0x0004(父类索引),0x0000(接口索引集合),分别代表:类索引为3,父类索引为4,接口集合大小为0。通过我们的javap -v
计算出来的常量池找到常量池中索引为3和4的表:
1.5 字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段包括类级变量和实例级变量,不包括方法内部声明的局部变量。
字段可以包括:
- 修饰符由字段的作用域(public、private、protected修饰符);
- 实例变量还是类变量(static)、可变性(final);
- 并发可见性(volatile);
- 是否被序列化(transient);
- 字段数据类型(基本类型、对象、数组);
- 字段名称。
各修饰符都是bool值,字段名称引用常量池中的常量来描述。
字段表结构 | ||
---|---|---|
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index(字段简单名称) | 1 |
u2 | descriptor_index(描述符) | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
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 |
简单名称、描述符、全限定名的含义:
- 简单名称:没有类型和参数修饰的方法或字段名称。
- 全限定名:把类全名中的“.”替换成“/”,为了使连续的多个全限定名之间不产生混淆。
- 描述符:用来描述字段的数据类型、方法的参数列表(数量、类型、顺序)和返回值。基本数据类型和代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。
描述符标识字符含义 | |
---|---|
标识字符 | 含义 |
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
V | void |
L | 对象类型 |
我们可以看到fields_count为0x0001,access_flags为0x0002,name_index_0x0005,descriptor_index为0x0006,attrbutes_count为0x0000。
1.6 方法表集合
和字段表相似,但访问标志和属性表集合的可选项中有些差异。
方法表结构 | ||
---|---|---|
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index(字段简单名称) | 1 |
u2 | descriptor_index(描述符) | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法访问标志 | ||
---|---|---|
标志名称 | 标志值 | 含义 |
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 | 方法是否是由编译器产生的桥接方法 |
ACC_VARARGES | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否native |
ACC_ABSTRACT | 0x0400 | 方法是否abstract |
ACC_STRICT | 0x0800 | 方法是否strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否有编译器自动产生 |
如上图所示,方法表容量(methods_count)为0x0002,访问标志位(access_flags):0x0001,名称索引(name_index):0x0007,描述符(descriptor_index):0x0008,属性表计数器(attributes_count):0x0001,属性名称索引(attribute_name_index):0x0009。
1.7 属性表集合
虚拟机规范预定义属性 | ||
---|---|---|
属性名称 | 使用位置 | 含义 |
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法声明的异常 |
EnclosingMethod | 类文件 | 仅当一个类作为局部类或匿名类时才拥有这个属性,这个属性用于标示这个类所在的外围方法 |
InnnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | JDK6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Signature | 类中、方法表中、字段表中 | 用于支持范型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息 |
SourceFile | 类 | 记录源文件名称 |
SourceDebugExtension | 类 | JDK1.6中新增的属性,SourceDebugExtension用于存储额外的调试信息。 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动产生的 |
LocalVariableTypeTable | 类 | 它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类、方法表、字段表 | 为动态注解提供支持,用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的。 |
RuntimeInvisibleAnnotations | 类、方法表、字段表 | 与RuntimeVisibleAnnotations相反用于指明哪些注解是运行时不可见的。 |
RuntimeVisibleParameterAnnotations | 方法表 | 与RuntimeVisibleAnnotations类似,作用对象为方法的参数。 |
RuntimeInvisibleParameterAnnotations | 方法表 | 作用与RuntimeInvisibleAnnotations类似,作用对象为方法的参数。 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类 | JDK7新增的属性,用于保存invokedynamic指令引用的引导方法限定符 |
RuntimeVisibleTypeAnnotations | 类、方法表、字段表、Code属性 | JDK 8新增的属性,用于指明哪些类注解是运行时可见的 |
RuntimeInvisibleTypeAnnotations | 类、方法表、字段表、Code属性 | JDK 8新增的属性,用于指明哪些类注解是运行时不可见的 |
MethodParameters | 方法表 | JDK 8新增的属性,用于支持将方法名称编译进Class文件中,并可运行时获取。 |
属性表结构 | ||
---|---|---|
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
参考
《深入理Java虚拟机》周志明