点赞关注,不再迷路,你的支持对我意义重大!
🔥 Hi,我是丑丑。本文 「Java 路线」| 导读 —— 他山之石,可以攻玉 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)
前言
-
Class 文件
是Java
技术体系的重要组成部分,在学习整个虚拟机的执行引擎之前,应该清楚Class 文件
的结构; - 这篇文章将带你理解
Class 文件
的基本结构,希望能帮上忙。
延伸文章
对于
Java
编译过程不了解,请阅读:《Java | 聊一聊编译过程(编译前端 & 编译后端)》对于
类加载
的流程不太了解,请阅读:《Java | 谈谈你对类加载过程的理解》
目录
1. 什么是 Class 文件?
定义:或称字节码,可以看作
Java 虚拟机
的可执行文件作用:对应于一个类 / 抽象类 / 接口的定义信息
意义:
Java 虚拟机 & Class 文件
共同构成了Java
无关性的基础-
来源
- 由
Java 源码
经过Java 编译器
编译后得到,以磁盘文件形式存在
- 由
- 由字节码生成技术
(如javassist / CGLib / ASM)
生成,以内存中二进制流形式存在
- 由字节码生成技术
# 咬文嚼字 #
虽然字节码不一定是以 “磁盘文件” 的形式存在,但是通常在很多文献 & 资料中会笼统地将字节码表述为
Class 文件
,这里不必钻牛角尖。
更多信息请务必阅读:《Java | 为什么 Java 实现了平台无关性》
2. Class 文件的大致结构
-
Class 文件
是一种强协议的紧凑型结构(遵循《Java 虚拟机规范》) -
Class 文件
有三种数据结构:无符号数、TVL、表,具体如下:
-
Class 文件
本质上也是一个表,大致结构如下图:
-
魔数:固定值
0xCAFEBABE
,用于鉴别为合法的Class 文件
-
版本号:表示
Class 文件
的目标版本号,高版本的虚拟机可以向前兼容旧版本Class 文件
-
访问标志:一个
u2
无符号数,用于表示本类 / 接口的访问信息。其中每个标志位的值与java.lang.reflect.Modifier
中的常量一一对应:
本类索引 & 父类索引 & 接口索引集:是一个索引值,(共经过 2 次索引后)指向常量池中一个
utf-8
编码的 全限定名,例如:java/lang/Object;
字段表集合:用于描述类或接口中声明的变量(包括类变量与成员变量)
方法表集合:用于描述类或接口中声明的方法(包括类方法与成员方法)
属性表集合:用于描述 Class 文件、字段表、方法表中携带的属性
下面,我将概括表中各个重点数据项的具体含义!
3. 常量池(const pool)
- 常量池中的每一项常量都是一个表
- 存放两种类常量:字面量(Literal)和 符号引用(Symbolic References)
符号引用(Symbolic References)是一个字符串类型的字面量(存储在常量池),它的作用是唯一地标示一个实体,最重要的特点如下:
平台无关性
这一点与Java
的特性是一脉相承的。符号引用与具体虚拟机实现内存布局无关,需要在运行期将符号引用转换为直接引用(Direct Reference),这个直接引用才是符号引用在虚拟机中的真实存在。唯一性
无歧义地标示一个目标,以方法为例,如果是本类中声明的方法,不需要添加类名(如:Method func:()V
);如果是其他类中声明的方法, 需要添加类名前缀(如:Method com/Base.func:()V
)。
常量池表结构有一个共同的特点,就是表结构的首元素是u1
的标志位,代表当前的常量类型,截止到Java 13
总共有 20 种常量:
4. 本类索引 & 父类索引 & 接口索引集
- 本类索引肯定存在,只有一个
-
Java
是类单继承,所以父类索引只有一个(特例:Object
的父类索引为 0) -
Java
是接口多继承,所以接口索引有零或多个
这三个索引值均指向常量池中CONSTANT_Class_info
常量,而CONSTANT_Class_info
常量本质上也是一个索引值,指向CONSTANT_Utf8_info
常量。经过 2次 索引,这三个索引最终指向对应 类 / 接口的全限定名
5. 字段表(field_info)
字段表用于描述类字段与实例字段,但只包括在本类中声明的字段,既不包括父类中声明的字段,也不包括方法内部声明的局部变量。要注意的是,编译器生成的字段是包括的,例如编译器会为非静态内部类添加外部类的引用字段。字段表的基本结构如下:
- access_flags:字段的访问标志位
- name_index:常量池索引,最终指向字段的简单名称,见第 8 节
- descriptor_index:常量池索引,最终指向字段的描述符,见第 8 节
- attributes_count & attributes:字段属性,为字段的附加信息,见第 8 节
6. 方法表(method_info)
方法表和字段表的设计是很相似的。方法表用于描述类方法与实例方法,但只包括在本类中声明的方法或者重写的方法,不包括父类或父接口中声明的方法。需要注意的是,编译器生成的方法是包括的,例如类构造器<clinit>()
与实例构造器<init>()
。方法表的基本结构如下:
可以看到,方法表和字段表的基本结构是完全一致的,此处不再赘述。需要特别指出的是,方法里面的代码在方法的Code属性
,方法的受检异常声明在Exception属性
。
7. 属性表(attribute_info)
- 属性相当于字段表或方法表的附加信息
- 每一项属性都是一个表,基本结构如下图所示:
-
attribute_name_index:常量池索引,最终指向一个属性名。
Class 文件
使用属性名来区分每一种属性,截止到Java 12
,总共有 29 种预定义属性。 - attribute_length:不同属性的属性信息是不同的,因此需要一个长度表表示属性信息占用的长度
- info:属性信息
# 你觉得呢?#
市面上你能找到的介绍虚拟机的书籍,普遍都会按顺序罗列出每种属性的信息。笔者并不是说这种方式不好,因为作为书籍的阐述方式需要考虑到读者可接受度 & 参考性的问题。但是如果以博客的阐述方式也采用同样地方式,岂非成为知识搬运工?因此,我将从不同的问题域出发,在每个问题域中介绍每种属性。
Code 属性
Exceptions 属性
LocalVariableTable 属性
LocalVariableTypeTable 属性
Signature 属性
泛型中所谓的类型擦除,其实只是擦除Code 属性
中的泛型信息,在类常量池属性中其实还保留着泛型信息,这也是在运行时可以反射获取泛型信息的根本依据。在这篇文章里,我们详细讨论:《Java | 关于泛型能问的都在这里了(含Kotlin)》,请关注!
RuntimeVisibleAnnotations 属性
RuntimeInvisibleAnnotations 属性
RuntimeVisibleParameterAnnotations 属性
RuntimeInvisibleParameterAnnotations 属性
注解在编译后擦除,如果注解的保留级别为CLASS & RUNTIME
,在 Class 文件中还会生成对应的注解属性。在这篇文章里,我们详细讨论:《Java | 这是一篇全面的注解使用攻略(含 Kotlin)》,请关注!
InnerClasses 属性
8. 信息描述规则
Editting...
参考资料
- 《深入理解Java虚拟机(第3版本)》(第6章)—— 周志明 著
- 《深入理解Android:Java虚拟机 ART》(第2章) —— 邓凡平 著
- 《深入理解 JVM 字节码》(第2、3、4章)—— 张亚 著
创作不易,你的「三连」是丑丑最大的动力,我们下次见!