以下图文基本来自周志明《深入理解Java虚拟机 第3版》
目录
类的结构
类型信息存储在jvm内存划分中的方法区。
class文件
class文件由相应编译器编译而来,用以虚拟机中执行:
Java技术能够一直保持着非常良好的向后兼容性, Class文件结构的稳定功不可没 .时至今日, Java发展经历了十余个大版本、 无数小更新, 1997年发布的《Java虚拟机规范》中定义的Class文件格式的各项细节几乎没有出现任何改变。尽管不同版本的《Java虚拟机规范》 对Class文件格式进行了几次更新, 但基本上只是在原有结构基础上新增内容、 扩充功能, 并未对已定义的内容做出修改。
class文件本质
Class文件是一组以8个字节为基础单位的二进制流, 各个数据项目严格按照顺序紧凑地排列在文
件之中, 中间没有添加任何分隔符, 这使得整个Class文件中存储的内容几乎全部是程序运行的必要数
据, 没有空隙存在。 当遇到需要占用8个字节以上空间的数据项时, 则会按照高位在前[2]的方式分割
成若干个8个字节进行存储。
任何一个Class文件都对应着唯一的一个类或接口的定义信息, 但是反过来说, 类或
接口并不一定都得定义在文件里(譬如类或接口也可以动态生成, 直接送入类加载器中) 。
class文件如何存储数据
根据《Java虚拟机规范》 的规定, Class文件格式采用一种类似于C语言结构体的伪结构来存储数
据, 这种伪结构中只有两种数据类型: “无符号数”和“表”。
-
无符号数:属于基本的数据类型, 以u1、 u2、 u4、 u8来分别代表1个字节、 2个字节、 4个字节和8个字节的无符号数, 无符号数可以用来描述数字、 索引引用、 数量值或者按照UTF-8编码构成字符串
值。 -
表:由多个无符号数或者其他表作为数据项构成的复合数据类型, 为了便于区分, 所有表的命名
都习惯性地以“_info”结尾。 表用于描述有层次关系的复合结构的数据。
整个Class文件本质上可视作是一张表, 按严格顺序排列构成如图:
该表就是class文件的结构,所有class文件内容都严格按照这样的结构组成。
无论是无符号数还是表, 当需要描述同一类型但数量不定的多个数据时, 经常会使用一个前置的
容量计数器加若干个连续的数据项的形式, 这时候称这一系列连续的某一类型的数据为某一类型的“集
合”
Class的结构不像XML等描述语言, 由于它没有任何分隔符号, 所以在上表中的数据项, 无论是顺序还是数量, 甚至于数据存储的字节序(Byte Ordering, Class文件中字节序为Big-Endian) 这样的细节, 都是被严格限定的, 哪个字节代表什么含义, 长度是多少,先后顺序如何, 全部都不允许改变。
class文件结构解析
魔数
每个Class文件的头4个字节被称为魔数(Magic Number) , 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。
不仅是Class文件, 很多文件格式标准中都有使用魔数来进行身份识别的习惯, 譬如图片格式, 如GIF或者JPEG等在文件头中都存有魔数。 使用魔数而不是扩展名来进行识别主要是基于安全考虑, 因为文件扩展名可以随意改动。 文件格式的制定者可以自由地选择魔数值, 只要这个魔数值还没有被广泛采用过而且不会引起混淆。
Class文件的魔数取值为0xCAFEBABE。
版本号
紧接着魔数的4个字节存储的是Class文件的版本号: 第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)
Java的版本号是从45开始的, JDK 1.1之后
的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号) , 高版本的JDK能向下兼容以前版本的Class文件, 但不能运行以后版本的Class文件, 因为《Java虚拟机规范》 在Class文件校验部分明确要求了即使文件格式并未发生任何变化, 虚拟机也必须拒绝执行超过其版本号的Class文件。
从JDK 1.1到13之间, 主流JDK版本编译器输出的默认的和可支持的Class文件版本
号:
关于次版本号, 曾经在现代Java(即Java 2) 出现前被短暂使用过, JDK 1.0.2支持的版本45.0~45.3(包括45.0~45.3) 。 JDK 1.1支持版本45.0~45.65535, 从JDK 1.2以后, 直到JDK 12之前次版本号均未使用, 全部固定为零。 而到了JDK 12时期, 由于JDK提供的功能集已经非常庞大, 有一些复杂的新特性需要以“公测”的形式放出, 所以设计者重新启用了副版本号, 将它用于标识“技术预览版”功能特性的支持。 如果Class文件中使用了该版本JDK尚未列入正式特性清单中的预览功能, 则必须把次版本号标识为65535, 以便Java虚拟机在加载类文件时能够区分出来。
常量池容量计数值(constant_pool_count)
紧接着主、 次版本号之后的是常量池入口, 常量池可以比喻为Class文件里的资源仓库, 它是Class
文件结构中与其他项目关联最多的数据, 通常也是占用Class文件空间最大的数据项目之一,还是在Class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量是不固定的, 所以在常量池的入口需要放置一项u2类型的数据, 代表常
量池容量计数值(constant_pool_count)
这个容量计数是从1而不是0开始的, 如果偏移地址: 0x00000008 为十六进制数0x0016, 即十进制的22, 这就代表常量池中有21项常量, 索引值范围为1~21
在Class文件格式规范制定之时, 设计者将第0项常量空出来是有特殊考虑的, 这样做的目的在于, 如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义, 可以把索引值设置为0来表示。 Class文件结构中只有常量池的容量计数是从1开始, 对于其他集合类型, 包括接口索引集合、 字段表集合、 方法表集合等的容量计数都与一般习惯相同, 是从0开始。
常量池
这里的常量池是class文件内的常量池,而不是平常讲的“运行时常量池”或“全局字符串常量池”。
每一项常量都是一个表,每个常量类型拥有其特定的表结构,一共十七种常量类型:
最初常量表中共有11种结构各不相同的表结构数据, 后来为了更好地支持动态语言调用, 额外增加了4种动态语言相关的常量, 为了支持Java模块化系统(Jigsaw),又加入了CONSTANT_Module_info和CONSTANT_Package_info两个常量, 所以截至JDK 13, 常量表中分别有17种不同类型的常量。这17类表都有一个共同的特点, 表结构起始的第一位是个u1类型的标志位(tag) , 是当前常量的类型标号。
比如class常量池中,某一项常量标志位为07,那么就表示该常量类型标志是07,属于上表中的CONSTANT_Class_info类型,代表一个类或者接口的符号引用。
CONSTANT_Class_info的结构:
- tag是标志位, 它用于区分常量类型;
- name_index是常量池的索引值, 它指向常量池中一个CONSTANT_Utf8_info类型常量, 此常量代表了这个类(或者接口) 的全限定名。
对于class文件中,标志位为07的那项常量来说,“07”后面跟的就是该常量的u2长度的name_index的值,这就是一项常量在class文件中的样子。
常量表分类
这十七种常量类型可被分成两大类,字面量和符号引用:
- 字面量(Literal):接近于Java语言层面的常量概念, 如文本字符串、 被声明为final的常量值等。
- 符号引用(Symbolic References):符号引用则属于编译原理方面的概念, 主要包括下面几类常量:
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法句柄和方法类型(Method Handle、 Method Type、 Invoke Dynamic)
- 动态调用点和动态常量(Dynamically-Computed Call Site、 Dynamically-Computed Constant)
Java代码在进行Javac编译的时候, 并不像C和C++那样有“连接”这一步骤, 而是在虚拟机加载Class
文件的时候进行动态连接 。 也就是说, 在Class文件中不会保存各个方法、 字段最终在内存中的布局信息, 这些字段、 方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址, 也就无法直接被虚拟机使用的。 当虚拟机做类加载时, 将会从常量池获得对应的符号引用, 再在类创建时或运行时解析、 翻译到具体的内存地址之中。
相关工具
在JDK的bin目录中, Oracle公司已经准备好一个专门用于分析Class文件字节码的工具: javap,其 -verbose参数输出的class文件字节码内容。
访问标志
在常量池结束之后, 紧接着的2个字节代表访问标志(access_flags) ,这个标志用于识别一些类或
者接口层次的访问信息, 包括: 这个Class是类还是接口; 是否定义为public类型; 是否定义为abstract类型; 如果是类的话, 是否被声明为final; 等等。
当前只定义了这9个位置,其他的位置直接置0.
类索引、 父类索引与接口索引集合
- 类索引(this_class) :一个u2类型的数据,用于确定这个类的全限定名
- 父类索引(super_class) :一个u2类型的数据,用于确定这个类的父类的全限定名
除了java.lang.Object之外, 所有的Java类都有父类, 因此除了java.lang.Object外, 所有Java类的父类索引都不为0
- 接口索引集合(interfaces):一组u2类型的数据的集合,用来描述这个类实现了哪些接
口这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口, 则应当是extends关键字) 后的接口顺序从左到右排列在接口索引集合中。【按这里的设计,说明接口继承接口的时候可以多继承!】
Class文件中由这三项数据来确定该类型的继承关系。
类索引和父类索引的查找过程
类索引和父类索引用两个u2类型的索引值表示, 它们各自指向常量池中一个类型为CONSTANT_Class_info的类描述符常量,然后通过常量池就能查找到各自的全限定名字符串。
接口索引的查找过程
对于接口索引集合, 入口的第一项u2类型的数据为接口计数器(interfaces_count) , 表示索引表
的容量(实现或继承了多少个接口)。如果该类没有实现任何接口, 则该计数器值为0, 后面接口的索引表不再占用任何字节。
然后还是从常量表查出后面的接口索引项的全限定名字符串。
字段表集合(field_info)
字段表用于描述接口或者类中声明的变量。 Java语言中的“字段”(Field) 包括类级变量以及实例级变量, 但不包括在方法内部声明的局部变量。
字段可以包括的修饰符有字段的作用域(public、 private、 protected修饰符) 、 是实例变量还是类变量(static修饰符) 、 可变性(final) 、 并发可见性(volatile修饰符, 是否强制从主内存读写) 、 可否被序列化(transient修饰符) 、 字段数据类型(基本类型、 对象、 数组) 、字段名称。
上述这些信息中, 各个修饰符都是布尔值, 要么有某个修饰符, 要么没有, 很适合使用标
志位来表示。
而字段叫做什么名字、 字段被定义为什么数据类型, 这些都是无法固定的, 只能引用常
量池中的常量来描述。
字段的结构
一个由五部分组成:
- 字段修饰符access_flags :
很明显, 由于语法规则的约束, ACC_PUBLIC、 ACC_PRIVATE、 ACC_PROTECTED三个标志最多只能选择其一, ACC_FINAL、 ACC_VOLATILE不能同时选择。 接口之中的字段必须有ACC_PUBLIC、 ACC_STATIC、 ACC_FINAL标志, 这些都是由Java本身的语言规则所导致的。
- name_index:对常量池项的引用, 代表字段的简单名称。
- descriptor_index:对常量池项的引用, 代表**(方法和)字段的描述符**(即字段的类型)
- 其他:字段表所包含的固定数据项目到descriptor_index为止就全部结束了,不过在descrip-tor_index之后跟随着一个属性表集合, 用于存储一些额外的信息, 字段表可以在属性表中附加描述零至多项的额外信息。 比如存在默认值的时候就可能会存在一项名称为ConstantValue的属性, 其值指向某常量。
- 全限定名:把类的全限定名中的“.”替换成了“/”, 为了使连续的多个全限定名之间不产生混淆, 在使用时最后一般会加入一个“; ”号表示全限定名结束。
- 简单名称:指没有类型和参数修饰的方法或者字段名称。
- 方法和字段的描述符:用来描述字段的数据类型、 方法的参数列表(包括数量、 类型以及顺序) 和返回值。
----基本数据类型(byte、 char、 double、 float、 int、 long、 short、 boolean) 以及代表无返回值的void类型都用一个大写字符来表示, 而对象类型则用字符L加对象的全限定名来表示。
-----对于数组类型, 每一维度将使用一个前置的“[”字符来描述, 如一个定义为“java.lang.String[][]”类型
的二维数组将被记录成“[[Ljava/lang/String; ”, 一个整型数组“int[]”将被记录成“[I”。
-----用描述符来描述方法时, 按照先参数列表、 后返回值的顺序描述, 参数列表按照参数的严格顺序
放在一组小括号“()”之内。 如方法void inc()的描述符为“()V”, 方法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”。
字段表集合中不会列出从父类或者父接口中继承而来的字段, 但有可能出现原本Java代码之中不
存在的字段, 譬如在内部类中为了保持对外部类的访问性, 编译器就会自动添加指向外部类实例的字
段。 另外, 在Java语言中字段是无法重载的, 两个字段的数据类型、 修饰符不管是否相同, 都必须使
用不一样的名称, 但是对于Class文件格式来讲, 只要两个字段的描述符不是完全相同, 那字段重名就
是合法的。
方法表集合(method_info)
结构除了code部份外和字段表很相似,摆几个表,不赘述了。
方法表结构:
方法访问表示位:
方法的代码部分,经过Javac编译器编译成字节码指令之后, 存放在方法属性表集合中一个名为“Code”的属性里面。
如果父类方法在子类中没有被重写(Override) , 方法表集合中就不会出现来自父类的方法信息。 但同样地, 有可能会出现由编译器自动添加的方法, 最常见的便是类构造器“()”方法和实例构造器“()”方法.
在Java语言中, 要重载(Overload) 一个方法, 除了要与原方法具有相同的简单名称之外, 还要求
必须拥有一个与原方法不同的特征签名。 特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合, 也正是因为返回值不会包含在特征签名之中, 所以Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。 但是在Class文件格式之中, 特征签名的范围明显要更大一些,只要描述符不是完全一致的两个方法就可以共存。 也就是说, 如果两个方法有相同的名称和特征签名, 但返回值不同, 那么也是可以合法共存于同一个Class文件中的。
属性表集合(attribute_info)
Class文件、 字段表、 方法表都可以携带自己的属性表集合, 以描述某些场景专有的信息。
与Class文件中其他的数据项目要求严格的顺序、 长度和内容不同, 属性表集合的限制稍微宽松一些, 不再要求各个属性表具有严格顺序, 并且《Java虚拟机规范》 允许只要不与已有属性名重复, 任何人实现的编译器都可以向属性表中写入自己定义的属性信息, Java虚拟机运行时会忽略掉它不认识的属性。
属性的表结构:
为了能正确解析Class文件, 《Java虚拟机规范》 最初只预定义了9项所有Java虚拟机实现都应当能识别的属性, 而在最新的《Java虚拟机规范》 的Java SE 12版本中, 预定义属性已经增加到29项:
Code属性
Java程序方法体里面的代码经过Javac编译器处理之后, 最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中, 但并非所有的方法表都必须存在这个属性, 譬如接口或者抽象类中的方法就不存在Code属性, 如果方法表有Code属性存在, 那么它也具有相应的结构: