【前提概要】
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文件的语义,只能提供附加的描述信息 。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。