JVM解析——类文件结构
本系列主要记录笔者在学习 [深入理解Java虚拟机] 一书时的理解
我们都知道在Java中,我们并不需要过多的在意内存的管理,这一切都交给了虚拟机自动管理,我们并不需要操心何时需要去释放一个对象的内存。
当然,如果出现了内存溢出或泄漏,我们就必须去了解一下Java虚拟机的内存管理机制以便于我们解决问题
[笔者仍为Android初学者。如有解释错误的地方,欢迎评论区指正探讨]
本篇为该系列第四篇,概述类文件结构。
概述
想要深入的了解Jvm,那么解析class文件是必不可少的,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。
接下来我们依照这张图,一步一步的解析类文件的结构:
魔数Magic Number
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行*身份识别,譬如图片格式,如gif或者jpeg等在文件头中都存有魔数。
使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。
版本号
紧接着魔数的是版本号,分为次版本号和主版本号,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
常量池
接下来是常量池,常量池可以理解为Class文件之中的资源仓库,
它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
由于常量池数量是不确定的,所以需要有一个u2类型的数据用来存储常量池数量。
常量池主要存放两大类常量:字面量和符号引用。字面量指的是文本字符串、声明为final的常量值等。而而符号引用则属于编译原理方面的概念,包括:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
常量池中每一项常量都是一个表,看一下图
常量池表里存储的各种类型的数据的一些信息,这里就不深入展开了。
访问标志
接下来的两个直接表示访问标志,这个标志用来识别一些类或接口层次的访问信息,包括:这个class是类还是接口,是不是public,是不是abstract,是不是final等等信息。
access_flags中一共有16个标志位可以使用,1为真,0为假。
类索引,父类索引,接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合。
Class文件中由这三项数据来确定这个类的继承关系。
- 类索引用于确定这个类的全限定名.
- 父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了
java.lang.Object
之外,所有的Java类都有父类,因此除了java.lang.Object
外,所有Java类的父类索引都不为0。 - 接口索引集合就用来描述这个类实现了哪些接口,接口索引集合还有一个标记(interface_count),用来标记实现了多少个接口。
这些索引值各自指向一个类型为CONSTANT_Class_info
的类描述符常量,通过CONSTANT_Class_info
类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info
类型的常量中的全限定名字符串。
字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
一个字段表中包含了字段的作用域(public、private、protected修饰
符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称等等信息。
大部分信息都是用bool值来表示的,是或者否。
而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
方法表集合
类似于字段表,方法表依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)等几项。
常规的标志都和字段表的标志一样,用bool值来表示。
不过不同于字段的是,方法还应该有方法代码,而这部分存放在属性表的code字段里。属性表是Class文件格式中最具扩展性的一种数据项目,下面我们来了解一下属性表。
属性表
属性表(attribute_info)在前面的讲解之中已经出现过数次,在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息
属性表中可以包括很多信息,比如方法代码(code),常量值(ConstantValue),异常(Exceptions),源文件名称(SourceFile)等等信息,对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。
小结
类文件结构大概就介绍到这里,每一种表都有各自的属性,但是无外乎都记录了我们原来java文件中写的一些属性,变量,方法的信息。这些信息多种多样,这里就不深入阐述,简单了解。