虚拟机是如何加载类的

1、概述

  首先先来看几个问题java

  • jvm是如何加载这些Class文件的?
  • jvm加载一个Class文件须要哪些步骤?
  • Class文件中的信息进入到虚拟机后会发生什么变化?

  接下来看看jvm加载class文件的概述:程序员

  jvm把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终造成能够被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。这句话差很少已经回答上面三个问题的大部分了。数组

  与那些在编译是须要进行链接工做的语言不一样,在Java语言里面,类型的加载和链接过程都是在程序运行期间完成的,这样会在类加载是稍微增长一些性能开销,可是却能为Java应用程序提供高度的灵活性,Java中能够动态的扩展的语言特性就是依赖运行期动态加载和动态链接这个特色实现的。好比编写一个使用接口的应用程序,能够等到运行时在指定其实际的实现。这种组装应用程序的方式普遍应用于Java程序之中。数据结构

2、要点

  类从被加载到jvm内存中开始,到卸载出内存为止,它的生命周期包括了一下步骤:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Useing)和卸载(Unloading)七个阶段。其中的验证、准备和解析三个部分统称为连接(Linking),这七个阶段的发生顺序以下图,注意是发生的顺序,不是执行完成的前后顺序。jvm

一、加载

  加载阶段是“类加载”过程的一个阶段,虚拟机须要作如下三件事:布局

  • 经过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
  • 在Java堆中生成一个表明这个类的java.lang.Class对象,做为方法区这些数据的入口。

  加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式有虚拟机实现自定义,虚拟机规范未规定此区域的具体数据结构。而后再Java堆中实例化一个java.lang.Class类的对象,这个对象做为程序访问方法区中的这些类型数据的外部接口。加载阶段与链接阶段的部份内容是交替进行的,加载阶段还没有完成,链接阶段可能已经开始,但这些夹在加载阶段之中进行的动做,仍然属于链接阶段的内容,这两个阶段的开始时间仍然保持着固定的前后顺序。性能

二、验证

  验证阶段虚拟机作了下面这些事情编码

  一、文件格式验证spa

  第一阶段是要验证字节流是否符合Class文件格式的规范,而且能被当前版本的虚拟机处理。会验证一下这些内容。指针

  • 主、次版本号是否在当前虚拟机处理范围以内。
  • 常量池的常量中是否有不被支持的常量类型。
  • 指向常量的各类索引值中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
  • Class文件中各部分及文件自己是否有被删除的或附加的其余信息

  二、元数据的验证

  • 这个类是否有父类。
  • 这个类的父类是否继承了不容许被继承的类(即被final修饰的类)。
  • 若这个类不是抽象类,是否实现了其父类或接口之中要求的全部方法。

  三、字节码的验证

  这个阶段是验证最为复杂的一个阶段,主要工做是进行数据流和控制流分析,紧接第二阶段。

  • 保证任意时刻操做数栈的数据类型与指令代码序列能配合工做。不会是在操做栈中放置了一个int类型的数据,使用时却按照long类型来加载。
  • 保证跳转指令不会跳转到方法体以外的字节码上。
  • 保证 方法体中的类型转换是有效。

  四、符号引用验证

  • 符号引用中经过字符串描述的全限定名是否能找到对应的类。
  • 在知道类中是否存在符合方法的字段描述符即简单名称所描述的方法和字段。
  • 符合引用中的类、字段和方法的访问级别是否能够被当前的类访问。

三、准备

  准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,注意是初始值不是最终变量的值,都将在方法区中进行分配。若是该变量不是静态变量,将不会进行内存分配,而是会在类出乎实话的时候随着对象一块儿分配到Java堆中。另外这里的初始值一般状况下是零值。具体的初始化的值见下图,图片来源于《深刻理解Jvm虚拟机》。

 四、解析

  解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

  •  符号引用:符号引用以一组符号来描述所引用的目标,符号可使任何形式的字面量,只要使用时能无歧义地定位到目标便可。符号引用于虚拟机实现内存布局无关,引用的目标不必定已经加载到内存中。

  •  直接引用:直接引用可使直接指向目标的指针、相对偏移量是一个能间接定位到目标的句柄。

 五、初始化

   类初始化阶段是类加载过程的最后一步,前面的类加载过程当中,除了在加载阶段用户应用程序能够经过自定义类加载器参与以外,其他动做彻底有虚拟机主导和空值。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

  前面讲到在准备阶段变量已经富余过一次初始值,而在初始化阶段,则是根据程序员经过程序制定主观计划去初始化变量和其余资源。

3、初始化阶段补充

  一下四中状况会必须当即对类进行“初始化”。

  • 遇到new、getstatic、putstatic或invokestatic者4调字节码指令时,若是类每一进行过初始化,则须要先触发器初始化。
  • 使用反射调用一个对象的时候,该对象必须初始化
  • 当初始化一个类的时候发现其父类没有初始化,则对其父类先初始化
  • 当虚拟机启动的时候,用户须要知道一个要执行的主类(即包含main()方法的那个类),虚拟机会先初始化这个主类。

  除了上面4中场景,都不会触发初始化,称为被动引用。

场景一

public class SupClass {

    public static int value = 100;
    
    static{
        System.out.println("SupClass init...");
    }
    
}


public class SubClass extends SupClass {
    
    static{
        System.out.println("SubClass init...");
        
    }
    
}

 客户端代码

public class InitTest {
    
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
    
}

 输出以下

SupClass init...
100

 能够看到经过子类引用父类的静态字段,不会致使子类初始化。

 场景二

 其余代码同场景一,客户端代码变成以下

public class InitTest {
    
    public static void main(String[] args) {
        SupClass[] sca = new SupClass[10];
    }
    
}

 这段代码不会输出任何结果。由于经过数组定义来引用类,不会触发此类的初始化。

 场景三

public class ConstClass {

    public static final String HELLO = "hello";
    
    static{
        System.out.println("ConstClass init...");
    }
    
}

 客户端代码

public class InitTest {
    
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLO);
    }
    
}

 输出以下

hello

 能够看到常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,所以不会触发定义常量的类的初始化。

4、总结

本篇文章依据如下两点

  • 在实际状况中,每一个class文件都有可能表明着Java语言中的一个类或者接口,而对于类和接口须要分开描述
  • 笔者所讲的“Class文件”并不是指class必须是存在于具体磁盘中的某个文件,这里说的class文件指的是一种二进制字节流,不管以何种形式存在均可以。
相关文章
相关标签/搜索