共计 5433 个字符,预计需要花费 14 分钟才能阅读完成。
【前提概要】
Java 源码文件通过编译(Compile)后生产 Class 字节码文件。JVM 时通过字节码来执行。对于深刻开掘和透彻了解 Java 技术体系,除了意识学习 Java API 相干的技术之外,最应该优先学习的技术 class 字节码文件的构造体系。接下来就让咱们深刻开掘学习 class 字节码吧 。
【跨平台性】
各种不同平台的虚拟机与所有平台都对立应用的程序存储格局——字节码(ByteCode)是形成平台无关性的基石,也是语言无关性的根底。Java 虚拟机不和包含 Java 在内的任何语言绑定,它只与“Class 文件”这种特定的二进制文件格式所关联,Class 文件中蕴含了 Java 虚拟机指令集和符号表以及若干其余辅助信息 。
【Class 文件】
任何一个 Class 文件都对应着惟一一个类或接口的定义信息,但反过来说,Class 文件实际上它并不一定以磁盘文件的模式存在。Class 文件是一组以 8 位字节为根底单位的二进制流 。
【Class 文件构造】
Class 文件格式采纳相似 C 语言构造体的伪构造来存储数据,这种伪构造中只有两种数据类型:“无符号数”和“表”。
【无符号概念】
【无符号数】属于根本的数据类型,以 u1、u2、u4、u8 来别离代表 1 个字节、2 个字节、4 个字节、8 个字节的无符号数,无符号数能够用来形容数字、索引援用、数量值或者依照 UTF- 8 编码形成字符串值 。
【表的概念】
【数据表】是由多个无符号数或其余表作为数据表形成的复合数据类型,为了与无符号数以及其余构造进行辨别,所有表的命名都习惯性的以“_info”结尾。例如,field_info、method_info、attribute_info 等 。
整个 Class 文件实质上就是一张表。理解 Class 文件的构造对理解虚拟机执行引擎有重要作用。
【构造严谨性】
Class 的构造不像 XML 等描述语言,因为它没有任何分隔符号,所以在其中的数据项,无论是程序还是数量,都是被严格限定的,哪个字节代表什么含意,长度是多少,先后顺序如何,都不容许扭转 。
【魔术头和版本号】
【魔术头】
- 魔数头是每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的惟一作用是确定这个文件是否为一个能被虚拟机承受的 Class 文件 。
- 应用魔数而不是扩展名来进行辨认次要是基于平安方面的思考,因为文件扩展名能够随便地改变。文件格式的制定者能够自在地抉择魔数值,只有这个魔数值还没有被宽泛采纳过同时又不会引起混同即可 。
- 所有的由 Java 编译器编译而成的 class 文件的前 4 个字节都是“0xCAFEBABE”(谐音咖啡宝贝)。
- 当 JVM 在尝试加载某个文件到内存中来的时候,会首先判断此 class 文件有没有 JVM 认为能够承受的“签名”,即 JVM 会首先读取文件的前 4 个字节,判断该 4 个字节是否是“0xCAFEBABE”,如果是,则 JVM 会认为能够将此文件当作 class 文件来加载并应用 。
- 不同版本的 Java 虚拟机实现反对的版本号也不同,高版本号的 Java 虚拟机实现能够反对低版本号的 Class 文件,反之则不成立 。
【主 / 次版本】
-
紧接着魔数的 4 个字节存储的是 Class 文件的版本号:
- 第 5 和第 6 个字节是次版本号(MinorVersion)。
- 第 7 和第 8 个字节是主版本号(Major Version)。
Java 的版本号是从 45(十进制)开始的,JDK1.1 之后的每个 JDK 大版本公布主版本号向上加 1 高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行当前版本的 Class 文件,即便文件格式并未产生任何变动,虚拟机也必须拒绝执行超过其版本号的 Class 文件。例如:JDK1.0 主版本号为 45,JDK1.1 为 46,顺次类推到 JDK8 的版本号为 52,16 进制为 0x33。
【版本解析流程】
- 一个 JVM 实例只能反对特定范畴内的主版本号(Mi 至 Mj)和 0 至特定范畴内(0 至 m)的副版本号。假如一个 Class 文件的格局版本号为 V,仅当 Mi.0≤v≤Mj.m 成立时,Class 文件才能够被此 Java 虚拟机反对 。
- JVM 在加载 class 文件的时候,会读取出主版本号,而后比拟这个 class 文件的主版本号和 JVM 自身的版本号,如果 JVM 自身的版本号 <class 文件的版本号,JVM 会认为加载不了这个 class 文件 。
- 会抛出咱们常常见到 ” java.lang.UnsupportedClassVersionError: Bad version number in .class file “Error 谬误;反之,JVM 会认为能够加载此 class 文件,持续加载此 class 文件 。
【常量池元数据】
常量池元数据次要蕴含了常量池数据表和常量池计数器。
【常量池计数器】
- 常量池是由一组 CP 构造体(cp_info)数据项组成的,而数据表的大小则由常量池计数器指定。
- 常量池计数器 constant_pool_count 的值是 constant_pool 表中的成员数 +1(永远执行下一个待调配的索引数据值)。 常量池中常量的数量是不固定的,所以在常量池的入口须要搁置一项 u2 类型的数据,代表常量池容量计数值(constant_pool_count)。
- constant_pool 表的索引值只有在大于 0 且小于 constant_pool_count 时才会被认为是无效的 。
-
注意事项:常量池计数器默认从 1 开始而不是从 0 开始,与 Java 中语言习惯不一样的是,这个容量计数是从 1 而不是 0 开始的
- 当 constant_pool_count = 1 时,常量池中的 cp_info 个数为 0;
- 当 constant_pool_count 为 n 时,常 量池中的 cp_info 个数为 n -1。
起因:在指定 class 文件标准的时候,将索引 #0 项常量空进去是有非凡思考的,这样当:某些数据在特定的状况下想表白“不援用任何一个常量池项”的意思时,就能够将其援用的常量的索引值设置为#0 来示意 。
【常量池数据表】
常量池数据表次要寄存两大类常量:字面量和符号援用。
-
常量池中次要寄存两大类常量:字面量(Literal)和符号援用(Symbolic References)。
- 字面量比拟靠近于 Java 语言层面的常量概念,如文本字符串、申明为 final 的常量值、根本变量等 。
- 符号援用则属于编译原理方面的概念,包含了上面三类常量:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、办法的名称和描述符、以及一些扩大援用信息。
常量池的结构图:
cp_info {
u1 tag;
u1 info[];}
JVM 是依据 tag 的值来确定常量池项 cp_ino 的类型字面量的。
【拜访标记】
在常量池完结之后,紧接着 2 个字节代表拜访标记(access_flag),这个标记用来辨认这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被申明为 final 等等。
【类索引、父类索引与接口索引汇合】
- Class 文件由 this_class、super_class 和 interfaces 这三项数据来确定该类的继承关系。
- 类索引用来确定类的全名限定,父类索引用来确定该类的父类的全名限定,接口索引汇合用来形容该类实现了哪些接口 。
【类索引】
- 类索引,this_class 的值必须是对 constant_pool 表中我的项目的一个无效索引值。constant_pool 表 在这个索引处的项必须为 CONSTANT_Class_info 类型常量,示意这个 Class 文件所定义的类或接口 。
【父类索引】
父类索引,对于类来说,super_class 的值必须为 0 或者是对 constant_pool 表中我的项目的一个无效索引值 。
- 如果它的值不为 0,constant_pool 在索引处的项必须 CONSTANT_Class_info 类型常量,示意这个 Class 文件所定义的类的间接父类。以后类的间接父类,以及它所有间接父类的 access_flag 中都不能带有 ACC_FINAL 标记。
- 对于接口来说,它的 Class 文件的 super_class 项的值必须是对 constant_pool 表中我的项目的一个无效索引值。constant_pool 表在这个索引处的项必须为代表 java.lang.Object 的 CONSTANT_Class_info 类型常量。
- 如果 Class 文件的 super_class 的值为 0,那这个 Class 文件只可能是定义的是 java.lang.Object 类,只有它是惟一没有父类的类。
【接口元数据】
- 接口计数器,interfaces_count 的值示意以后类或接口的【间接父接口数量】。
- 接口数据表,interfaces[] 数组中的每个成员的值必须是一个对 constant_pool 表中我的项目的一个无效索引值,它的长度为 interfaces_count。每个成员 interfaces[i] 必须为 CONSTANT_Class_info 类型常量,其中【0 ≤ i <interfaces_count】。
- 在 interfaces[] 数组中,成员所示意的接口程序和对应的源代码中给定的接口程序(从左至右)一样,即 interfaces[0] 对应的是源代码中最右边的接口。
【字段表汇合】
字段表(field_info)用来形容接口或类中申明的变量 。
- 字段包含的修饰符有字段的作用域(public、private、protected 修饰符)、是实例变量还是类变量(static 修饰符)、可变性(final)、并发可见性(volatile 修饰符、是否强制从主内存读写)、可否被序列化(transient 修饰符)、字段数据类型(根本类型、对象、数组)、字段名称 。
- 字段计数器,fields_count 的值示意以后 Class 文件 fields[] 数组的成员个数。fields[] 数组每一项都是一个 field_info 构造的数据项,它用于示意该类或接口申明的【类字段】或者【实例字段】(但不包含从父类或父接口继承的局部)。
【办法表汇合】
- 办法计数器,methods_count 的值示意以后 Class 文件 methods[] 数组的成员个数。Methods[] 数组中每一项都是一个 method_info 构造的数据项。
- 办法表,methods[] 数组中的每个成员都必须是一个 method_info 构造的数据项,用于示意以后类或接口中某个办法的残缺形容。
- method_info 构造能够示意类和接口中定义的所有办法,包含【实例办法】、【类办法】、【实例初始 化办法】和【类或接口初始化办法】。methods[] 数组只形容【以后类或接口中申明的办法】,【不包含从父类或父接口继承的办法】。
- 办法表的构造与字段表一样,顺次包含拜访标记(access_flags)、名称索引(name_index)、描述符索引(description_index)、属性表汇合(attributes)几项 。
- 因为 volatile 关键字和 transient 关键字不能润饰办法,所以办法表的拜访标记中没有 ACC_VOLATILE 标记和 ACC_TRANSIENT 标记,与之绝对,synchronized、native、strictfp 和 abstract 关键字能够润饰办法 。
- 如果某个 method_info 构造的 access_flags 项既没有设置 ACC_NATIVE 标记也没有设置 ACC_ABSTRACT 标记,那么它所对应的办法体就该当能够被 Java 虚拟机间接从以后类加载,而不须要援用其它类 。
【属性表汇合】
- 属性计数器,attributes_count 的值示意以后 Class 文件 attributes 表的成员个数。attributes 表中每一项都是一个 attribute_info 构造的数据项。
- 属性表,attributes 表的每个项的值必须是 attribute_info 构造。在 Java 7 标准里,Class 文件构造中的 attributes 表的项包含下列定义的属性:
在 Class 文件、字段表、办法表都能够带本人的属性表汇合。
- 1、Code 属性
- 2、Exceptions 属性
- 3、LineNumberTable 属性
- 4、LocalVariableTable 及 LocalVariableTypeTable 属性
- 5、SourceFile 及 SourceDebugExtension 属性
- 6、ConstantValue 属性
- 7、InnerClasses 属性
- 8、Deprecated 及 Synthetic 属性
- 9、StackMapTable 属性
- 10、Signature 属性
- 11、BootstrapMethods 属性
- 12、MethodParameters 属性
- 13、模块化相干属性
- 14、运行时注解相干属性
- 对于反对 Class 文件格式版本号为 49.0 或更高的 Java 虚拟机实现,必须正确辨认并读取 attributes 表中的 Signature、RuntimeVisibleAnnotations 和 RuntimeInvisibleAnnotations 属性。对于反对 Class 文件格式版本号为 51.0 或更高的 Java 虚拟机实现,必须正确辨认并读取 attributes 表中的 BootstrapMethods 属性。
- Java 7 标准 要求 任一 Java 虚拟机实现能够主动疏忽 Class 文件的 attributes 表中的若干(甚至全副)它不可 辨认的属性项。任何本标准未定义的属性不能影响 Class 文件的语义,只能提供附加的形容信息。