JVM扫盲-2:虚拟机执行子系统

一、类文件结构

Java虚拟机只与Class文件相关联,它规定了Class文件应该具备的格式,而不论该文件是由什么语言编写并编译而来。因此,任何语言只要可以最终编译成符合Java虚拟机要求的Class文件,就能够运行在Java虚拟机上面。就是说,不管是使用Java, Scala, Kotlin, Groovy仍是其余语言,只要编译出的Class文件符合虚拟机规范,那么均可以被虚拟机执行。因此,实际上Java规范就是由Java语言规范和Java虚拟机规范两个独立的部分组成。java

Class类文件是一种二进制文件,它包含了Java虚拟机指令集和符号表以及若干其余辅助信息。Class文件格式采用相似于C的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表。无符号数属于基本数据类型,以u1,u2,u4,u8分别表明1字节、2字节、4字节和8字节的无符号数,无符号数能够用来描述数字、索引、数量值或者按照utf-8编码构成的字符串。bash

表是由多个无符号数或者其余做为表做为数据项构成的复合数据结构,全部表习惯性地以"_info"结尾,整个Class文件本质上是一张表。而所谓的表就对应于C++中的一个结构体,好比整个Class文件对应的结构体就是:网络

struct ClassFile {
    u4 magic;                // 识别Class文件格式,具体值为0xCAFEBABE,
    u2 minor_version;        // Class文件格式副版本号,
    u2 major_version;        // Class文件格式主版本号,
    u2 constant_pool_count;  // 常量表项个数,
    cp_info **constant_pool; // 常量表,又称变长符号表,
    u2 access_flags;         // Class的声明中使用的修饰符掩码,
    u2 this_class;           // 常数表索引,索引内保存类名或接口名,
    u2 super_class;          // 常数表索引,索引内保存父类名,
    u2 interfaces_count;     // 超接口个数,
    u2 *interfaces;          // 常数表索引,各超接口名称,
    u2 fields_count;         // 类的域个数,
    field_info **fields;     // 域数据,包括属性名称索引,
    u2 methods_count;        // 方法个数,
    method_info **methods;   // 方法数据,包括方法名称索引,方法修饰符掩码等,
    u2 attributes_count;         // 类附加属性个数,
    attribute_info **attributes; // 类附加属性数据,包括源文件名等。
};
复制代码

上面结构体的各个变量的定义的顺序与Class文件中的存储顺序是一致的。下面咱们用一个二进制编辑器打开Class文件并简单看下,Class文件中的存储如何与上面的结构体对应的。数据结构

Class二进制文件

根据结构体的定义,首先是magic字段,它是u4类型的,即4字节,对应于上图中的CAFEBABE;紧接着两个u2类型的字段,minor_versionmajor_version,用来表示Class文件的版本号,对应于上图中的00000031;而后是一个u2类型的constant_pool_count,用来表示常数表项个数,这里是0027,也就是有38个常量,所以常量从1开始计数的;接着常量个数的是变长符号表,这个符号表的长度就是以前常量的长度,即38,而后咱们要按照常量的规则找到38个常量以后就是u2类型的访问标志。并发

那这38个常量又如何寻找呢?实际上,Class文件中总计规定了14种常量结构体,每种结构体都包含一个tag字段,它是u1类型的,即1个字节,而且存储在该结构体的首位。咱们须要先根据该tag字段找到该常量的定义,而后才能肯定接下来的几个字节是属于该常量的。好比上图中的第一个常量的tag0A,咱们能够查询相关的表知道它是CONSTANT_Fleddef_info类型的常量,该常量有3个字段:u1类型的tag, u2类型的index, u2类型的index,故咱们能够肯定常量长度以后的5个字节是属于第一个常量的。依次,分析下去咱们能够获得第二个,第三个等常量的信息。获得了常量的信息以后,就能够获取上面结构体中其余的字段的信息。编辑器

其实,按照上面的分析,咱们能够看出分析Class文件时一个很是机械的过程,由于它有固定的规则在里面,因此咱们可使用命令行工具javap来获取以上的信息。在实际开发过程当中从字节码的角度分析问题的情形可能并很少,可是了解字节码中的一些指令,尤为是并发相关的指令,对学习和分析问题都大有裨益。工具

二、类加载机制

2.1 类加载过程

Java程序中的类加载是在运行期间完成的,咱们可使用Java预约义的加载器或者自定义加载器动态从各类渠道加载类并使用。最初将类加载器从虚拟机中分离出来是为了Applet等的动态加载,可是后来随着技术发展,动态加载被应用到各类场景中,好比移动端的插件化、热补丁、Tomcat的类加载等各类场景中,因此类加载是很是重要的一块内容。布局

一个类从被加载到虚拟机内存到卸载的整个生命周期包括:加载-验证-准备-解析-初始化-使用-卸载7个阶段。其中验证-准备-解析3个阶段称为链接。学习

加载发生在类被使用的时候,若是一个类以前没有被加载,那么就会执行加载逻辑,好比当使用new建立类、调用静态类对象和使用反射的时候等。加载过程主要工做包括:1).从磁盘或者网络中获取类的二进制字节流;2).将该字节流的静态存储结构转换为方法取的运行时数据结构;3).在内存中生成表示这个类的Class对象,做为方法区访问该类的各类数据结构的入口。ui

验证阶段会对加载的字节流中的信息进行各类校验以确保它符合JVM的要求。

准备阶段会正式为类变量分配内存并设置类变量的初始值。注意这里分配内存的只包括类变量,也就是静态的变量(实例变量会在对象实例化的时候分配在堆上),而且这里的设置初始值是指‘零值’,好比int类型的会被初始化为0,引用类型的会被初始化为null,即便你在代码中为其赋了值。

解析阶段是将常量池中的符号引用替换为直接引用的过程。符号引用与虚拟机实现的布局无关,引用的目标并不必定要已经加载到内存中。各类虚拟机实现的内存布局能够各不相同,可是它们能接受的符号引用必须是一致的,只要能正肯定位到它们在内存中的位置就行。直接引用能够是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。若是有了直接引用,那引用的目标一定已经在内存中存在。

初始化是执行类构造器<client>方法的过程。<client>方法是由编译器自动收集类中的类变量的赋值操做和静态语句块中的语句合并而成的。虚拟机会保证<client>方法执行以前,父类的<client>方法已经执行完毕。

2.2 类加载机制 双亲委派模型

类加载器用来根据类的全限定名获取描述此类的二进制字节流。咱们能够把类加载器分红如下4种:

  1. 启动类加载器:存在于虚拟机中,使用C++编写,负责将<Java_Home>/lib下面的类库加载到内存中(好比rt.jar);
  2. 拓展类加载器:它负责将<Java_Home>/lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中;
  3. 应用程序类加载器:它负责将用户类路径中指定的类库加载到内存中;
  4. 自定义类加载器:就是指用户本身定义的类加载器。

在Java种存在以上多种类加载器,它们之间经过必定的规则相互配合,这个规则就是双亲委派模型:每一个类加载器收到类加载请求时,都会先将请求委派给父类加载器去完成,因此,加载请求会一直传递到最顶层的类加载器。只有当类父类加载器没法完成加载请求的时候,该加载器才会本身去加载。固然,这不是强制的,咱们也能够彻底使用本身的一套逻辑。但双清委派模型的好处就在于,假如你自定义了一个类java.lang.Object,那么当使用双亲委派模型来加载的时候,会由子加载器不断向上传递加载请求到启动类加载器进行加载,所以Object在各类类加载环境中都是同一个类。这保障了Java系统的稳定性。

三、虚拟机字节码执行引擎

虚拟机是相对于物理机而言的,它们的区别是物理机的执行引擎直接创建在处理器、硬件、指令集和操做系统层面上,而虚拟机的执行引擎是本身实现的。因此,执行引擎也是Java虚拟机最核心的组成部分之一。

执行引擎用来执行咱们写的业务逻辑,而业务逻辑就是指一些方法,因此虚拟机执行引擎就是用来执行各个方法的。而方法是经过栈帧来描述的,方法的执行是用栈帧入栈和出栈来描述的,栈帧中存储了方法的局部变量表、操做数栈、动态链接和方法返回地址等信息。因此说,执行引擎就是用来执行各个栈帧的。在虚拟机执行的时候,只有最顶层的栈帧是有效的,与之关联的方法就称为当前方法,而且执行引擎运行的全部字节码都是针对当前栈帧的。

方法的信息存储在栈帧中,而栈帧中的方法信息是从Class文件中读取来的。回到以前的Class类文件结构部分,每一个方法是经过结构体method_info来描述的:

struct method_info
{
    u2 access_flags;         //方法修饰符掩码
    u2 name_index;           //方法名在常数表内的索引
    u2 descriptor_index;     //方法描述符,其值是常数表内的索引
    u2 attributes_count;     //方法的属性个数
    attribute_info **attributes;    //方法的属性数据,
};
复制代码

method_info中又包含了一个属性表集合attribute_info类型的attributes,方法的局部变量表须要的空间大小和操做栈的深度等就记录在其中。局部变量表用于存放方法参数和方法内的局部变量,当方法是实例方法的时候,局部变量表的第0位会被用来传递方法所属对象的引用,即this。Java虚拟机执行引擎是基于栈的执行引擎,这里的栈就是操做数栈。操做数栈的深度也是记录在方法属性集合的Code属性中的。

执行引擎的执行过程

咱们能够用下面的一个程序来讲明如下在实际的执行过程当中,Java执行引擎是如何工做的。

public int cal() {
    int a = 100;
    int b = 200;
    int c = 300;
    return (a + b) * c; // 1
}
复制代码

首先会在编译期肯定方法的栈深度和局部变量表的长度,栈深度是由计算的过程获得的,而局部变量表的长度等于1个this加3个局部变量即4。当程序执行到1处时,局部变量表内会被填充为this100200300。程序计数器会随着代码当前执行到的位置而不断更新。而此时由于没有进行任何计算,因此栈仍是空的。

接下来就开始进行计算:首先会把a压入栈中(其实时把局部变量表里的值压入栈中);而后把b压入栈中;接着将栈顶的两个元素先出栈,相加以后再入栈,此时栈中只有一个计算结果300;接下来再把c压入栈中;而后,把栈顶的两个元素出栈,执行完乘法以后再入栈,因此最终栈中只有一个90000;最后,使用ireturn指令结束方法并将栈顶的元素返回给方法的调用者。

总结

以上就是虚拟机执行子系统的一个过程,包含了从编译出的Class文件,到被加载到内存中、验证、初始化等,到最终在虚拟机中被执行等完整的过程。这里只是总结和梳理了相关的基础的知识点,在虚拟机中实际的执行过程确定远比咱们上述的内容更加复杂和精彩。

相关文章
相关标签/搜索