1.前言
当编写完一段Java代码并保存以后,其实Java代码会保存在以.Java为扩展名作为结尾的文件中,如test.java,而这个文件若想在JVM上执行,则必须先利用javac编译器进行编写,形成所谓的“字节码(ByteCode)文件”,即接下来要分析的重点内容---Class类文件(.class文件)。然后,JVM虚拟机才会执行.class文件。
具体过程如上图所示
而Java虚拟机想做的,并不仅仅只针对Java语言,因此,在设计JVM之初,设计者便决定Java虚拟机将不和包括Java在内的任何语言绑定,Java虚拟机只和字节码文件(Class文件,一种特定的二进制文件格式)所关联。从而,体现了Java虚拟机的一个重要特性——语言无关性。
由次可见,无论采用何种程序语言,只需要有对应的编译器将代码文件编译成统一的字节码文件,便可以在java虚拟机上运行。
2.Class类文件结构
由上图可知,所有编译器编译之后的最终结果是字节码文件,即Class文件,因此,本节就对Class文件进行讲解。
2.1 Class类文件的定义:
Class文件是一组以8位字节为基础单位的二进制流,各个数据项严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。这样的安排可以使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
2.2 Class类文件的内部结构
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的微结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节和8个字节,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以info结尾,表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,表结构如下所示。
类 型 | 名 称 | 数 量 | 备 注 |
u4 | magic | 1 | 魔数 |
u2 | minor_version | 1 | 次版本号 |
u2 | mmajor_version | 1 | 主版本号 |
u2 | constant_pool | constant_pool_count-1 | 常量池数量 |
cp_info | constant_pool | 1 | 常量池的集合内容 |
u2 | access_flags | 1 | 访问控制符 |
u2 | this_class | 1 | 类索引 |
u2 | super_class | 1 | 父类索引 |
u2 | interfaces_count | 1 | 接口索引数量 |
u2 | interfaces | interfaces_count | 接口数量 |
u2 | fields_count | 1 | 字段表集合数量 |
field_info | fields | fields_count | 字段表集合内容 |
u2 | methods_count | 1 | 方法表集合数量 |
method_info | methods | methods_count | 方法表集合 |
u2 | aattributes_count | 1 | 属性表集合数量 |
attribute_info | attributes | attributes_count | 属性表集合内容 |
这里强调一点,Class的结构不同于XML等描述语言,由于它没有任何分隔符号,所以在上表中的数据项,无论是数量还是顺序,甚至于数据存储的字节序这样的细节都被严格限定,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许被改变。
2.2.1 魔数
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用就是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中,都使用魔数进行身份识别,这里需要注意的是,用魔数来代替扩展名进行识别主要是基于安全方面的考虑,因为文件的扩展名可以随意改动,而文件格式的制定者可以自由地选择魔数,只要这个魔数还没有被广泛采用过同时又不会引起混淆即可。例如,Class文件的魔数就是0xCAFEBABE。
2.2.2 Class文件版本
魔数之后的四个字节,分别存储Class文件的版本号:第5和第6字节存储次版本号(Minor Version),第7和第8字节存储主版本号(Major Version)。Java版本号是从45开始,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
2.2.3 常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,踏实Class文件与其他项目关联最多的数据类型,也是占Class文件空间最大的数据项目之一,同时,它还是在Class文件中第一个出现的表数据类型项目。
由于常量池中的常量数是不确定的,因此常量池入口处需要放置一个u2类型的数据,代表常量池容量计数值(constant_pool_count,其实就是统计一下常量池里有多少个常量),这里值得注意的是,这个容量计数从1而非从0开始,因此,常量池的实际容量应该是constant_pool_count-1。
例如常量池容量为0x0016,即十进制的22,代表常量池中有21项常量。索引值范围为1~21,。
而设计者将第0项常量空出来,是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况下就可以把索引值置为0来表示。
常量池计数值之后,紧跟着的就是常量池内描述各项常量的表。常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。字面量比较接近于Java语言层面的常量概念,例如文本字符串、final常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
1. 类和接口的全限定名(Fully Qualified Name)
2. 字段的名称和描述符(Descriptor)
3. 方法的名称和描述符
Java代码在进行编译的时候,区别于C/C++,它并没有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接,也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,此案次这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法被虚拟机直接使用。
当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时对符号引用进行解析、翻译到具体的内存地址之中。
常量池中的每一项常量都是一个表,在JDK1.7之前一共有11种结构各不相同的表结构数据,在JDK1.7中为了更好地支持动态语言调用,又额外增加了三种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info、CONSTANT_InvokeDynamic_info)。
这14种表都有一个共同特点,就是表开始的第一位是一个u1类型的标志位(Tag),代表当前这个常量数据哪种常量类型。这14种常量类型所代表的的具体含义如下所示。
综上,常量池的组织可以说比较简单清晰,即前端2个字节用来表示常量池计数器(constant_pool_count),它用于记录常量池的组成元素——常量池项的个数。
由上图可知,cp_info(常量池项)的数据结构为表,数据类型就是上表中的所有类型。
所以,我们可以将cp_info进一步替换成具体的表类型,如下所示。
当然,常量池的内容不仅仅是上述所说的,如果想进一步了解,可以参考《Java虚拟机原理图解》 1.2.2、Class文件中的常量池详解(上),讲的非常浅显易懂。
2.2.4 访问标志、类索引、父类索引、接口索引集合
在常量池结束之后,紧跟着的就是Class文件中的访问标志、类索引、父类索引以及接口索引集合。具体如下所示。
2.2.4.1. 访问标志
访问标志(access_flag)紧接着常量池,占用2个字节,也就是16位,如下图所示。访问标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
当JVM在编译某个类或者接口的源代码时,JVM会解析这个类或者接口的访问标志信息,然后将这些标志设置到访问标志(access_flag)着16个位上,JVM会参考如下访问表示信息。
a. 我们知道,每个定义的类或者接口都会生成class文件,这里也包括一些内部类,在某个类中定义的静态内部类也会单独生成一个class文件。
对于定义的类,JVM在将其编译成class文件时,会将class文件的访问标志的第11位设置为1,该位叫做ACC_SUPER标志位
对于定义的接口,JVM在将其编译成class文件时,会将class文件的访问标志的第8位设置为1,该位叫做ACC_INTERFACE标志位
b. class文件表示的类或者接口的访问浅显有public类型和包package类型。
如果累或者接口被声明为public类型的,那么JVM在将其编译成class文件时,会将class文件的访问标志的第16位设置为1,该位叫做ACC_PUBLIC标识位;
c. 类是否为抽象类型的,即我们定义的类是否被abstract关键字所修饰。
如果,我们声明如下类:
1. public abstract class MyClass{......}
那么根据上文a,b所提及的内容可知,JVM将MyClass这个Java类编译成class文件的时候,会将class文件的访问标志的第7位设置为1,第7位叫做ACC_ABSTRACT标志位,同时,如果JVM编译的对象是接口的话,也会将class文件的class文件的访问标志的第7位设置为1。
d. 该类是否声明了final类型,即表示此类不能用于继承。
此时,JVM会在编译class文件的过程中,将class的访问标志的第12位设置为1。第12位叫做ACC_FINAL标志位。
e. 如果我们这个class文件不是JVM通过Java源代码文件编译而成的,而是用户自己通过class文件的组织规则生成的,那么一般会对class文件的访问标志的第4位设置为1,通过JVM编译源代码产生的class文件此标志位为0,第4位叫做ACC_SYTHETIC标志位。
f. 枚举类,如果对于定义的枚举类,形如
public enum EnumTest{....}
JVM也会将此枚举类编译成class文件,这时,对于这样的class文件,JVM会对访问标志的第2位设置为1,以表示它是一个枚举类,第2位叫做ACC_ENUM标志位
g. 注解类,对于定义的注解类,如public @interface{......},JVM会对此注解类编译成class文件,对于这样的class文件,JVM会将访问标志第3位设置为1,以表示这个类为注释类,第3位叫做ACC_ANNOTATION标志位
当JVM确定了上述标志位的值以后,就可以确定访问标志(access_flag)的值了,实际上,JVM上述标志会根据上述确定的标志位的值,对这些标志位的值取或,便得到了访问标志(access_flag)。
举例:定义一个最简单的类Simple.java,使用编译器编译成class文件,然后观察class文件中的访问标志的值,以及使用javap -v Simple 查看访问标志。
1. package com.louis.jvm;
2.
3. public class Simple {
4.
5. }
使用UltraEdit查看编译成的class文件,如下图所示:
上述的图中黄色部分表示的是常量池部分,具体为什么是常量池部分不是本文的重点,有兴趣的读者可以参考我的《Java虚拟机原理图解》系列关于常量池的博客,你就可以很轻松地识别常量它们了。
常量池后面紧跟着就是访问标志,它的十六进制值为0x0021,二进制的值为:00000000 00100001,由二进制的1的位数可以得出第11、16位为1,分别对应ACC_SUPER标志位和ACC_PUBLIC标志位。
也可以通过一下运算:
0x0021 = 0x0001 | 0x0020, 即: 访问标志表示的标志是ACC_PUBLIC + ACC_SUPER
为了验证我们的运算,使用javap -v Simple查看反编译信息如下:(小技巧:使用javap -v Simple指令的结果展示在命令提示符下显示不友好,一般我是使用javap -v Simple > temp.txt,将结果重定向到文件中,然后查看文件)
2.2.4.2. 类索引、父类索引与接口索引集合
1. 类索引
我们知道,一般情况下一个Java类源文件经过JVM编译会生成一个class文件,也有可能一个Java类源文件中定义了其他类或者内部类,这样编译出来的class文件就不止一个,但每一个class文件表示某一个类,值域这个class表示哪个类,就由“类索引”这个数据项来确定。JVM通过类的完全限定名确定是某一个类。
类索引的作用,就是为了指出class文件所描述的这个类叫什么名字。
类所有紧接着访问标志的后面,占有2个字节,在这两个字节中存储的值是一个指向常量池的一个索引,该索引指向的是CONSTANT_Class_info常量池项。
以上面定义的Simple.class 为例,如下图所示,查看他的类索引在什么位置和取什么值。
由上可知,它的类索引值为0x0001,那么,它指向了常量池中的第一个常量池项,那我们再看一下常量池中的信息。使用javap -v Simple,常量池中有以下信息:
可以看到常量池中的第一项是CONSTANT_Class_info,它表示一个“com/louis/jvm/Simple”的类名。即类索引告诉我们这个class文件所表示的是哪一个类。
2. 父类索引
java支持单继承模式,除了java.lang.Object类之外,每一个类有且只有一个父类。class文件中紧接着类索引(this_class)之后的两个字节区域表示父类索引,跟类索引一样,父类索引这两个字节中的值指向了常量池中的某个常量池项CONSTANT_Class_info,表示该class是继承自哪一个类
3. 接口索引集合
一个类可以不实现任何接口,也可以实现很多接口,为了表示当前类实现的接口信息,class文件使用了如下结构体描述某个类的接口实现信息:
由于类实现的接口数目不确定,因此接口索引集合的描述的前部分叫接口计数器(interfaces_count),接口计数器占两个字节,其中的值表示这个类实现了多少个接口,紧跟着接口计数器的部分就是接口索引部分了,每一个接口索引占有2个字节,接口计数器的值代表着后面跟着的接口索引的个数,接口索引和类索引和父类索引一样,其内的值存储指向了常量池中的常量池项的索引,表示这个接口的全限定名。
举例:
Worker接口,然后类Programmer实现这个Worker接口,然后我们观察Programmer的接口索引集合是怎样表示的。
1. /**
2. * Worker 接口类
3. * @author luan louis
4. */
5. public interface Worker{
6.
7. public void work();
8.
9. }
1. package com.louis.jvm;
2.
3. public class Programmer implements Worker {
4.
5. @Override
6. public void work() {
7. "I'm Programmer,Just coding....");
8. }
9. }