类文件结构详解
引言
计算机是不能直接运行java代码的,要先由jvm运行编译后成字节码。字节码文件再交由运行于不同平台上的JVM虚拟机去读取执行,从而实现一次编写,到处运行的目的。
❓ 为什么计算机不能直接运行java代码呢
java是高级语言,我们才能理解其逻辑,计算机是无法识别的,所以java代码要先编译成字节码文件,jvm将字节码转换为计算机能识别的指令才能运行
简介
class文件本质上是一个以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在class文件中。
- jvm根据其特定的规则解析该二进制数据,从而得到相关信息。
- Class文件采用一种伪结构来存储数据,它有两种类型:无符号数和表。
Class文件结构
根据 Java 虚拟机规范,Class 文件通过 ClassFile
定义,有点类似 C 语言的结构体。
ClassFile
的结构如下:
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类会可以有多个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
魔数
🆎 (Magic Number)
每个 Class 文件的头 4 个字节称为魔数,唯一作用是检查这个文件是否为一个能被虚拟机接收的 Class 文件。
u4 magic; //Class 文件的标志
Class 文件版本号
🆎 (Minor&Major Version)
紧接着魔数的四个字节存储的是 Class 文件的版本号:第 5 和第 6 位是次版本号,第 7 和第 8 位是主版本号。
- 每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1
- 高版本的jvm可以执行低版本编译器生成的class文件,但是低版本的jvm不能执行高版本编译器生成的class文件。所以得确保开发环境和生产环境的JDK版本一致
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
常量池
🆎 (Constant Pool)
常量池主要存放两大常量:
- 字面量:类似java语言层面的常量概念
- 文本字符串
- 声明为 final 的常量值
- 基本数据类型的值
- 其他
- 符号引用:属于编译原理方面的概念
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型.
类型 | 标志(tag) | 描述 |
---|---|---|
CONSTANT_utf8_info | 1 | UTF-8 编码的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |
CONSTANT_MothodType_info | 16 | 标志方法类型 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
.class
文件可以通过javap -v class类名
指令来看一下其常量池中的信息(javap -v class类名-> temp.txt
:将结果输出到 temp.txt 文件)。
访问标志
🆎 (Access Flags)
在常量池结束之后,紧接着的两个字节代表访问标志,用于**识别一些类或者接口层次的访问信息**(这个 Class 是类还是接口,是否为 public
或者 abstract
类型,如果是类的话是否声明为 final
等等)
访问标志的含义如下:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为Public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义. |
ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 标志这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标志这是一个注解 |
ACC_ENUM | 0x4000 | 标志这是一个枚举 |
我们定义了一个 Employee 类
package top.snailclimb.bean;
public class Employee {
...
}
通过javap -v class类名
指令来看一下类的访问标志。
当前类、父类、接口索引集合
🆎 (This Class):类索引用于确定当前类的全限定名
🆎 (Super Class):父类索引用于确定这个类的父类的全限定名(Java是单继承,所以只有一个,除了除了 java.lang.Object
之外,所有类都有父类)
🆎 (Interfaces):接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 implements
(如果这个类本身是接口的话则是extends
) 后的接口顺序从左到右排列在接口索引集合中。
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
字段表集合
🆎 (Fields):存放多个字段表
字段表(field info)用于描述类或者接口声明的变量,包括类级变量(static
)以及实例变量(普通成员变量),不包括在方法内部声明的局部变量。
field info(字段表) 的结构:
- access_flags: 字段的作用域(
public
,private
,protected
修饰符),是实例变量还是类变量(static
修饰符),可否被序列化(transient
修饰符),可变性(final
),可见性(volatile
修饰符,是否强制从主内存读写)。 - name_index: 对常量池的引用,表示的字段的名称;
- descriptor_index: 对常量池的引用,表示字段和方法的描述符;
- attributes_count: 一个字段还会拥有一些额外的属性,attributes_count 存放属性的个数;
- attributes[attributes_count]: 存放具体属性具体内容。
上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型这些都是无法固定的,只能引用常量池中常量来描述。
u2 fields_count;//Class 文件的字段的个数
field_info fields[fields_count];//一个类会可以有个字段
字段的 access_flag 的取值:
方法表集合
🆎 (Methods)
methods_count 表示方法的数量,而 method_info 表示方法表。
Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。
method_info(方法表的) 结构:
u2 methods_count;//Class 文件的方法的数量
method_info methods[methods_count];//一个类可以有个多个方法
⚡️ 注意:
因为volatile
修饰符和transient
修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了synchronized
、native
、abstract
等关键字修饰方法,所以也就多了这些关键字对应的标志。
方法表的 access_flag 取值:
属性表集合
(Attributes)
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
参考文章
https://snailclimb.gitee.io/javaguide/#/./docs/java/jvm/class-file-structure