Class类的本质

任何一个Class文件都对应着唯一一个类或接口的定义信息, 但反过来说, Class文件实际上它并不一定是以磁盘文件的形式存在. Class文件是一组以8位字节为基础单位的二进制流

Java语言之外发展出一大批运行在Java虚拟机之上的语言,如Kotlin、Clojure、Groovy、JRuby、JPython、Scala等
实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息

Java语言中的各种语法、关键字、常量变量和运算符号的语义最终都会由多条字节码指令组合来表达,这决定了字节码指令所能提供的语言描述能力必须比Java语言本身更加强大才行。
因此,有一些Java语言本身无法有效支持的语言特性并不代表在字节码中也无法有效表达出来,这为其他程序语言实现一些有别于Java的语言特性提供了发挥空间
image.png

参考官文文档: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html



Class文件格式

类型 名称 说明 长度 数量
u4 magic 魔数,识别Class文件格式 4个字节 1
u2 minor_version 副版本号 2个字节 1
u2 major_version 主版本号 2个字节 1
u2 constant_pool_count 常量池计数器 2个字节 1
cp_info constant_pool 常量池 n个字节 constant_pool_count-1
u2 access_flags 访问标识 2个字节 1
u2 this_class 类索引 2个字节 1
u2 super_class 父类索引 2个字节 1
u2 interfaces_count 接口计数器 2个字节 1
u2 interfaces 接口索引集合 2个字节 interfaces_count
u2 fields_count 字段计数器 2个字节 1
fields_info fields 字段表 n个字节 fields_count
u2 methods_count 方法计数器 2个字节 1
method_info methods 方法表 n个字节 methods_count
u2 attributes_count 属性计数器 2个字节 1
attribute_info attributes 属性表 n个字节 attributes_count
  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值

  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表,这张表由图表所示的数据项按严格顺序排列构成

将一个class字节码文件解析

image.png

1.魔数

magic
每个Class文件的头4个字节称为魔数(magic),它的唯一作用是判断该文件是否为一个能被虚拟机接受的Class文件。它的值固定为0xCAFEBABE。

2. 版本号

minor_version & major_version
紧接着magic的4个字节存储的是Class文件的次版本号和主版本号,第5个和第6个字节所代表的含义就是编译的副本号minor_version , 而第7和第8个字节就是编译的主版本号major_version
image.png

版本号和Java编译器的对应关系如下表:

image.png
高版本的JDK能向下兼容低版本的Class文件,但不能运行更高版本的Class文件

3.常量池

3.1 constant_pool_count

常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(
constant_ pool_count)来告诉虚拟机后面的constant_pool数据有多少。与ava中语言习惯不一样的是这个容量计数是从1而不是0开始的

这是为了满足后面某些只想常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目”的含义, 这种情况可用索引值0来表示.

image.png
比如上面的值换算成十进制即22.
那么实际只有21项常量, 索引的范围是1-21

3.2 constant_pool

常量池表中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。这部分类容将在类加载后进入方法区的运行时常量池中存放
字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
而符号引用则属于编译原理方面的概念,主要包括下面几类常量

  • 被模块导出或者开放的包(Package)
  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符·方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
  • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-ComputedConstant)


    虚拟机在加载Class文件时才会进行动态连接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。

    这里说明下符号引用和直接引用的区别与关联:

    • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
    • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

常量池中每一项常量都是一个表,最初常量表中共有11种结构各不相同的表结构数据,后来为了更好地支持动态语言调用,额外增加了4种动态语言相关的常量,为了支持Java模块化系统(Jigsaw),又加入CONSTANT_Module_info和CONSTANT_Package_info两个常量,所以截至JDK 13,常量表中分别有17种不同类型的常量。

常量类型和结构

image.png
这17种常量类型各自均有自己的结构。
CONSTANT_Class_info型常量的结构中有一项name_index属性,该常属性中存放一个索引值,指向常量池中一个CONSTANT_Utf8_info类型的常量,该常量中即保存了该类的全限定名字符串。
CONSTANT_Fieldref_infoCONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_info型常量的结构中都有一项index属性,存放该字段或方法所属的类或接口的描述符CONSTANT_Class_info的索引项。

另外,最终保存的诸如Class名、字段名、方法名、修饰符等字符串都是一个CONSTANT_Utf8_info类型的常量,也因此,Java中方法和字段名的最大长度也即是CONSTANT_Utf8_info型常量的最大长度,在CONSTANT_Utf8_info型常量的结构中有一项length属性,它是u2类型的,即占用2个字节,那么它的最大的length即为65535。因此,Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译。

以上面的demo为例,解析过程如下
1.0a,转换成十进制即10,对应的是CONSTANT_Methodref_info,一共占5个节点
2.其后第2第3个节点, 其值00 04 image.png转成十进制为04, 引用的位置为image.png , 第一个值07 对应的CONSTAN_Integer_info
3.CONSTAN_Integer_info里的信息是image.png 00 15转成进制为21, 对应最后一部分(蓝色部分), image.png翻译后的信息为java/lang/object
4.其后第4第5个节点, 其值image.png 00 12 转成十进制为18, 对应的引用为, 第一个值0c转换十进制为12 ,其值对应的是CONSTANT_NameAndType_info
5.CONSTANT_NameAndType_info里面的信息是image.png, 分别指向为 CONSTANT_Class_info和CONSTANT_String_info
6.CONSTANT_Class_info的值为 3c 69 6e 69 , 翻译后为
7.CONSTANT_String_info的值为28 29 56,翻译后为()v
8.接下来就第2个, 依次类推
image.png

常量池中为什么要包含这些内容?
Java代码在进行 Javac编译的时候,并不像C和C++那样有“连接”这一步骤, 而是在虚拟机加载C1ass文件的时候进行动态链接。也就是说, 在C1ass文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

4.访问标识

access_flag
在常量池结束之后,紧接着的2个字节代表访问标志(access_flag),这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型,abstract类型,如果是类的话,是否声明为final,等等。每种访问信息都由一个十六进制的标志值表示,如果同时具有多种访问信息,则得到的标志值为这几种访问信息的标志值的逻辑或。
image.png

1.带有 ACC_INTERFACE标志的c1ass文件表示的是接口而不是类, 反之则表示的是类而不是接口

  • 如果一个class文件被设置了 ACC_INTERFACE标志, 那么同时也得设置 ACC_ABSTRACT标志。同时它不能再设置 ACC_FINAL ,ACC_SUPER 或 ACC ENUM标志
  • 如果没有设置 ACC_INTERFACE标志,那么这个c1ass文件可以具有上表中除 ACC_ANNOTATION外的其他所有标志。当然, ACC_FINAL和ACC_ABSTRACT这类互斥的标志除外。这两个标志不得同时设置

2.ACC_SUPER标志用于确定类或接口里面的invokespeciaL指令使用的是哪一种执行语义。针对Java虚拟机指令集的编译器都应当设置这个标志。对于Java se8及后续版本来说, 无论c1ass文件中这个标志的实际值是什么,也不管c1ass文件的版本号是多少,Java虚拟机都认为每个c1ass文件均设置了 ACC_SUPER标志

ACC_SUPER标志是为了向后兼容由旧]ava编译器所编译的代码而设计的。目前的 ACC SUPER标志在由JDK1.8.2之前的编译器所生成的 access_flags中是没有确定含义的,如果设置了该标志,那么 Oracle的]ava虚拟机实现会将其忽略

  1. ACC_YNTHETIC标志意味着该类或接口是由编译器生成的,而不是由源代码生成的
    4.注解类型必须设置 ACC_ANNOTATION标志。如果设置了 ACC ANNOTATION标志,那么也必须设置ACC_INTERFACE标志。
    5. ACC_ENUM标志表明该类或其父类为枚举类型。
    6.表中没有使用的 access_Flags标志是为未来扩充而预留的,这些预留的标志在编译器中应该设置为8,Java虚拟机实现也应该忽略它们

5.类索引、 父类索引、接口索引集合

this_class 、super_class 、interfaces

类索引(this_class)和 父类索引(super_class)都是一个u2类型的数据,
而接口索引集合(interfaces)则是一组u2类型的数据集合,Class文件中由这三项数据来确定这个类的继承关系。

类索引、父类索引和接口索引集合都按照顺序排列在访问标志之后,类索引和父类索引两个u2类型的索引值表示,它们各自指向一个类型为COMNSTANT_Class_info的类描述符常量,通过该常量中的索引值找到定义在COMNSTANT_Utf8_info类型的常量中的全限定名字符串。

而接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是个接口,则应当是extend语句)后的接口顺序从左到右排列在接口的索引集合中。

类索引查找全限定名的过程

image.png

6.fields

字段表(field_info)用于描述接口或类中声明的变量。字段包括了类级变量或实例级变量,但不包括在方法内声明的变量。字段的名字、数据类型、修饰符等都是无法固定的,只能引用常量池中的常量来描述。
下面是字段表的最终格式:
image.png
access_flags
字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型,
其中可以设置的标志位和含义如表所示
image.png

很明显,由于语法规则的约束,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,这些都是由Java本身的语言规则所导致的

name_index和descriptor_index
它们都是对常量池项的引用,分别代表字段的简单名称及字段和方法的描述符。这里简单解释下“简单名称”、“描述符”和“全限定名”这三种特殊字符串的概念。

全限定名即指一个事物的完整的名称,如在org.lxh.test包下的TestClass类的全限定名为:org/lxh/test/TestClass,即把包名中的“.”改为“/”,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“,”来表示全限定名结束。

简单名称则是指没有类型或参数修饰的方法或字段名称,如果一个类中有这样一个方法boolean get(int name)和一个变量private final static int m,则他们的简单名称则分别为get()和m。

描述符**的作用则是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序等)和返回值的。
根据描述符规则,详细的描述符标示字的含义如下表所示

描述符标识字符含义

image.png
对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个整数数组“int [][]”将为记录为“[[I”,而一个String类型的数组“String[]”将被记录为“[Ljava/lang/String”
image.png

用方法描述符描述方法时,按照先参数后返回值的顺序描述,参数要按照严格的顺序放在一组小括号内,如方法 int getIndex(String name,char[] tgc,int start,int end,char target)的描述符为“(Ljava/lang/String[CIIC)I”。

字段表包含的固定数据项目到descriptor_index为止就结束了,但是在它之后还紧跟着一个属性表集合用于存储一些额外的信息。比如,如果在类中有如下字段的声明:staticfinalint m = 2;那就可能会存在一项名为ConstantValue的属性,它指向常量2。

最后需要注意一点:字段表集合中不会列出从父类或接口中继承而来的字段,但有可能列出原本Java代码中不存在的字段。比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

7.methods

方法表(method_info)的结构与属性表的结构相同。方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里.

image.png

方法访问标志

image.png

与字段表集合相对应,如果父类方法在子类中没有被覆写,方法表集合中就不会出现来自父类的方法信息。但同样,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“”方法和实例构造器“”方法。

在Java语言中,要重载一个方法,除了要与原方法具有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。

8.attributes

属性表(attribute_info)在前面已经出现过多系,在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。
为了能正确解析Class文件,《Java虚拟机规范》最初只预定义了9项所有Java虚拟机实现都应当能识别的属性,而在最新的《Java虚拟机规范》的Java SE 12版本中,预定义属性已经增加到29项,这些属性具体见表6-13。后文中将对这些属性中的关键的、常用的部分进行讲解。
image.png

image.png

8.1.Code属性

前面已经说过,Java程序方法体中的代码讲过Javac编译后,生成的字节码指令便会存储在Code属性中,但并非所有的方法表都必须存在这个属性,比如接口或抽象类中的方法就不存在Code属性。如果方法表有Code属性存在,那么它的结构将如下表所示:
image.png
attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表了该属性的名称。
attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共是6个字节,所以属性值的长度固定为整个属性表的长度减去6个字节。
max_stack代表了操作数栈深度的最大值
max_locals代表了局部变量表所需的存储空间,它的单位是Slot,并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用。
code_lengthcode用来存储Java源程序编译后生成的字节码指令。code用于存储字节码指令的一系列字节流,它是u1类型的单字节,因此取值范围为0x00到0xFF,那么一共可以表达256条指令,目前,Java虚拟机规范已经定义了其中200条编码值对应的指令含义。code_length虽然是一个u4类型的长度值,理论上可以达到2^32-1,但是虚拟机规范中限制了一个方法不允许超过65535条字节码指令,如果超过了这个限制,Javac编译器将会拒绝编译。

exception_table是这个方法的显式异常处理表集合,它对于Code属性来说并不是必须存在的。它的格式如下表所示:
image.png
它包含四个字段,这些字段的含义为:如果字节码从第start_pc行到第end_pc行之间(不含end_pc行)出现了类型为catch_type或其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理,当catch_pc的值为0时,代表人和的异常情况都要转到handler_pc处进行处理。

异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常即finally处理机制,也因此,finally中的内容会在try或catch中的return语句之前执行,并且在try或catch跳转到finally之前,会将其内部需要返回的变量的值复制一份副本到最后一个本地表量表的Slot中.

Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码和元数据两部分,那么在整个Class文件里,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。

8.2 Exception属性

  1. 这里的Exception属性的作用是列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。它的结构很简单,只有attribute_name_indexattribute_lengthnumber_of_exceptionsexception_index_table四项,从字面上便很容易理解,这里不再详述。

8.3 LineNumberTable属性

它用于描述Java源码行号与字节码行号之间的对应关系。

8.4 LocalVariableTable属性

它用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的对应关系。

8.5 SourceFile属性

它用于记录生成这个Class文件的源码文件名称。

8.6 ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static修饰的变量才可以使用这项属性。在Java中,对非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量(static变量),则有两种方式可以选择:在类构造其中赋值,或使用ConstantValue属性赋值。<br />    目前Sun Javac编译器的选择是:如果同时使用final和static修饰一个变量(即全局常量),并且这个变量的数据类型是基本类型或String的话,就生成ConstantValue属性来进行初始化(编译时Javac将会为该常量生成ConstantValue属性,在类加载的准备阶段虚拟机便会根据ConstantValue为常量设置相应的值),如果该变量没有被final修饰,或者并非基本类型及字符串,则选择在<clinit>方法中进行初始化。<br />    虽然有final关键字才更符合”ConstantValue“的含义,但在虚拟机规范中并没有强制要求字段必须用final修饰,只要求了字段必须用static修饰,对final关键字的要求是Javac编译器自己加入的限制。因此,在实际的程序中,只有同时被final和static修饰的字段才有ConstantValue属性。而且ConstantValue的属性值只限于基本类型和String,很明显这是因为它从常量池中也只能够引用到基本类型和String类型的字面量。<br />   ** 下面简要说明下final、static、static final修饰的字段赋值的区别:**
  • static修饰的字段在类加载过程中的准备阶段被初始化为0或null等默认值,而后在初始化阶段(触发类构造器)才会被赋予代码中设定的值,如果没有设定值,那么它的值就为默认值。
  • final修饰的字段在运行时被初始化(可以直接赋值,也可以在实例构造器中赋值),一旦赋值便不可更改;
  • static final修饰的字段在Javac时生成ConstantValue属性,在类加载的准备阶段根据ConstantValue的值为该字段赋值,它没有默认值,必须显式地赋值,否则Javac时会报错。可以理解为在编译期即把结果放入了常量池中。

    7)InnerClasses属性
    该属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那么编译器将会为它及它所包含的内部类生成InnerClasses属性。
    8)Deprecated属性和Synthetic属性
    该属性用于表示某个类、字段和方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@Deprecated注释进行设置。
    9)Synthetic属性
    该属性代表此字段或方法并不是Java源代码直接生成的,而是由编译器自行添加的,如this字段和实例构造器、类构造器等。