【前提概要】

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文件的语义,只能提供附加的形容信息 。