JVM之Class结构详解
Class文件规范
根据《JAVA虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中(Class文件格式中)只有两种数据类型:“无符号数”和“表”。
本篇文章图中贴出的类型这一列也都是这两个数据类型,如果类型是u开头的表示为无符号数的数据类型,如果是_info结尾的表示是表类型。
- 无符号数就是正常的数字,以u1,u2,u4,u8来表示一个,两个,四个,八个字节,通常用来描述数字,索引引用,数量值或者用来表达字符串值。
u1在Class文件中为两个数字或字母,比如00,01,0A,6F ;
u2在Class文件中为四个数字或字母,比如0001,000A,6C61;
- 表是用来描述复合结构的数据,它由无符号数或者其他表构成,并在结尾处用“_info”表示。
比如:cp_info为常量池表,field_info为字段表,method_info为方法表,attribute_info为属性表。
本篇文章会讲解除了属性表之外的其他表结构,属性表单独写一篇文章进行讲解
- 本质上Class结构也是将表和无符号数组合起来的一张表。
Class文件大结构总览
后面讲的所有结构都是按照这个Class文件表讲的,所以这张图先看下。
magic
Class文件的头四个字节表示魔数,这个值存在于各个文件中,在之前的这篇文章里面讲过这个字段:验证apk文件的magic,magic是用来验证是不是属于某种结构的,即使用户更改了文件的后缀名也没事,通过这个字段就可以看出他是什么类型的文件。因此每一种文件格式的魔数magic都不一样,因为是用来表示其属于哪种文件格式的。
Class文件的magic为0XCAFFBABE(咖啡宝贝)
Class文件版本号
接下来的两个字节表达的Class文件的次版本号(Minor Version),再接着两个字节表达的是主版本号(Major Version)。
高版本的JDK能够兼容低版本的Class文件,但是低版本的JDK不能运行高版本的Class文件(规范中Class文件校验必须要求的!)
Constant_Pool常量池
1、Class文件中第一个出现的表结构的数据,主要存放两种常量:字面量和符号引用。
字面量:Java语言层面常量的概念。
eg:字符串,被声明为final的常量等
符号引用:编译原理方面的概念。eg:
被模块导出或者开放的包(Package)
类和接口的全限定名(也就是类所属的包名和类的名字)
字段的名称和描述符(名称简单理解就是字段名,描述符为字段所属的类型)
方法的名称和描述符(名称简单理解就是方法名,描述符为方法参数和返回值)
方法句柄和方法类型(Method Handle,Method Type,Invoke Dynamic)
动态调用点和动态常量
2、符号引用,由于Class文件是在编译后生成的,因此运行的时候需要将符号引用转换为对应数据在内存中的地址,否则使用的时候会找不到真正的内存入口地址(也就是不能被虚拟机直接使用)。
当虚拟机做类加载的时候,会从常量池中拿出对应的符号引用,在类创建或者运行时解析,翻译到具体的内存地址之中
比如class文件的类名,当类被加载到虚拟机中时,需要将这个符号引用转换为类真实所在的内存地址才能被正常访问。
3.常量池中存储的数据数量从1开始计数,第0项常量空出来是因为有些数据并不需要引用常量池中的数据时,就可以用第0项常量来表达。
4.常量池的数据数量会在版本号之后用两个字节来表达,之后才是常量池中的数据
这样做是因为没有Class文件中没有分隔符来表示一组数据结束,因此Class文件中的数据必须严格按照固定长度,固定数量去存放,不允许改变。
5.常量池中每一项常量数据都是一张表,每个表结构中都有一个u1类型的标志位标识自己属于哪种类型的表。
各个表对应的含义
这些表有着各自完全独立的结构,之间没有任何关联。
反编译代码示例
C:\>javap -verbose TestClass Compiled from "TestClass.java" public class org.fenixsoft.clazz.TestClass extends java.lang.Object SourceFile: "TestClass.java" minor version: 0 major version: 50 Constant pool: const #1 = class #2; // org/fenixsoft/clazz/TestClass const #2 = Asciz org/fenixsoft/clazz/TestClass; const #3 = class #4; // java/lang/Object const #4 = Asciz java/lang/Object; const #5 = Asciz m; const #6 = Asciz I; const #7 = Asciz <init>; const #8 = Asciz ()V; const #9 = Asciz Code; const #10 = Method #3.#11; // java/lang/Object."<init>":()V const #11 = NameAndType #7:#8;// "<init>":()V const #12 = Asciz LineNumberTable; const #13 = Asciz LocalVariableTable; const #14 = Asciz this; const #15 = Asciz Lorg/fenixsoft/clazz/TestClass;; const #16 = Asciz inc; const #17 = Asciz ()I; const #18 = Field #1.#19; // org/fenixsoft/clazz/TestClass.m:I const #19 = NameAndType #5:#6; // m:I const #20 = Asciz SourceFile; const #21 = Asciz TestClass.java;
代码块中Asciz就是之前说的Constant_UTF8_info常量(也就是字符串常量),可以看到这些数据都是由这个常量来组成的,
常量池中常量表对应数据结构内容
第一列常量为之前各个表对应的含义中提到的表结构类型也就是表的名称;
第二列中的项目为各个表中存放的不同的数据内容有哪些,这些表的数据中都有一个标识位tag用来标识不同的表,在之前各个表对应的含义中也提到过;
第三列为描述不同于之前讲的表的描述,这列说明的是表中存储的不同数据的描述
图例:
access_flags访问标识
常量池结束后就是这个Class对应的访问标志内容,用u2(两个字节)来表示。
访问标志可以有多个,根据多个标志位的或运算来决定最终的access_flags的取值
图例:
class的继承实现关系
访问标志之后的是this_class,super_class,interfaces_count,interfaces这些数据。
1.this_class:该Class的权限定名。(通过访问常量池中的类信息表来定位到类的权限定名)
2.super_class:父类的全限定名。(由于Java中是单继承多实现的,所以父类索引只有一个,但是接口可以有多个稍后将),跟上面的寻找过程类似,只不过换了一个常量池的索引,如果没有显示使用继承的话那么最终引用的是Object类的权限定名,如果明确使用了继承自哪个类那么最终引用到的就是那个类的权限定名
3.interfaces和interfaces_count
刚刚说了Class是可以多实现的,所以
interfaces_count为该Class实现接口的数量(如果没有任何实现的接口,那么该值为0后面的interfaces将没有数据不占用任何字节);
interfaces为实现接口的对应接口的全限定名
字段表
该表结构用于描述接口/类中声明的变量。
变量可以分为类变量(static级别的),实例变量。
注意:该部分不包括方法中的局部变量字段,局部变量会有一个单独的局部变量表来存储,下一篇文章讲解。对于Class文件中最外层的字段表中存储的只是类变量和实例变量。
而字段有哪些数据需要表示呢?
权限修饰符,字段名称,字段数据类型,类级别还是实例级别(是否有static修饰符),可变性(final),并发可见性(volatile修饰符,强制从主内存读写),是否可以被序列化(transient修饰符)
图例:
接下来一个一个进行说明
access_flags访问标识
这个访问标识和前面讲的类的access_flags不一样,这个是字段的访问标识
名称索引和描述符的索引
这两个索引都是在常量池中的索引,对常量池的引用
name_index为字段的名称
descriptor_index为描述符,对应于字段来说是该字段的数据类型
描述符的含义:
对于一维数组来说使用“[”来表示,如果是二位就用两个“[[”。
比如类型为String的二维数组(String[][])用描述符来表示就是[[ java.lang.String;一维数组“int[]”就是[I
存储额外信息
可以看到描述符后面还有一个属性表集合,该集合是用来存储一些额外的信息,可以添加0到多个信息。
如果没有额外描述的信息attributes_count就是0,后面没有字节去描述属性表;
如果有额外的信息比如将字段声明为final的话,属性表中就会存储一个ConstantValue的属性,这个值指向对应的常量值。(关于这部分内容之后进行详解)
该字段表中不会记录父类中或者父接口中继承来的字段,但是有可能出现原本不存在的字段,比如在内部类中添加字段来访问外部类。
重载字段:对于Class文件来说刚刚看了,只要描述符不一致,字段名一样也是可以被记录到Class中的而不会引起冲突;但是对于Java语言来说字段是不能重载的,不管描述符一样不一样,名称必须不一样!!!
方法表
该表结构用于描述接口/类中声明的变量
图例:
可以看到和上面讲的字段表结构大致是一样的。
access_flags访问标识
名称索引和描述符的索引
这两个索引都是在常量池中的索引,对常量池的引用
name_index为方法的名称
descriptor_index为描述符,对应于方法来说是该方法的参数集合和返回值类型
对应的数据在上面讲字段表的时候已经贴过图,不在说明。
存储额外信息
这个里面的属性表中就会有各种各样的表来存储一个方法的额外信息,比如方法体中的代码,局部变量表等等表结构数据类型。(下一篇文章进行详解)
和字段表一样如果父类方法没有被子类重写,那么该表中不会记录父类方法的信息的,但是同样可以出现一些编译器添加的方法,比如类构造器clvinit()和实例构造器init()
重载方法:名称一样,方法的特征签名不一样。
对于Class文件来说方法的特征签名不同(描述符不完全一致)就可以进行重载不会发生冲突;
但是对于Java语言来说方法的特征签名中不包括返回值这个条件所以返回值不同不能作为重载的条件
Java代码层面的方法特征签名,Java代码的方法特征签名只包括方法名称、参数顺序及参数类型,而字节码Class的特征签名还包括方法返回值以及受查异常表,请读者根据上下文语境注意区分。
属性表
下一篇文章进行详细讲解!