[Java]虚拟机执行子系统

  "深入理解Java虚拟机-虚拟机执行子系统"

Posted by Stephen.Ri on May 14, 2020

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

类文件结构

平台无关性

各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石。

Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。

Java虚拟机语言无关性

Class类文件结构

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。Class文件的魔数的获得很有“浪漫气息”,值为:0xCAFEBABE(咖啡宝贝?)。

字节码简介

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。

虚拟机类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

类型的加载、连接和初始化过程都是在程序运行期间完成的,Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

类加载的时机

Java类的生命周期如下:

Java类的生命周期

有且只有以下5种情况,会立即对类进行初始化:

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

类加载的过程

加载

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)

验证

  1. 文件格式验证:是否以魔数开头等
  2. 元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件
  4. 符号引用验证:可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验

准备

正式为类变量分配内存并设置类变量(static变量)初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

初始化

根据程序员通过程序制定的主观计划去初始化类变量和其他资源,

类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

双亲委派模型

类加载器 是否可以被Java程序引用 职责
启动类加载器 加载存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库
扩展类加载器 加载<JAVA_ HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库
应用程序类加载器 加载用户类路径(ClassPath)上所指定的类库

双亲委派模型

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

TODO 虚拟机字节码执行引擎