虚构机类加载机制

前言

Class文件结构早已学习完结,明日来学习下虚拟机怎样加载Class文件。

C语音编译连接后一向就生成了可执行文件,程序执行,并不需求额外操作。但Java不同,类型的加载和三番五次都是在程序运行时期成功的。

那会导致额外的费用,但那也给Java带来了无以复加的油滑,比如动态加载技术正是依据此特性成就的。

本文紧要首要内容如下:

  • 类加载时机
  • 类加载进程
  • 类加载器

类加载时机

类从被加载到虚拟机内存起头,到卸载出内存甘休,一共会经历
加载、验证、准备、解析、初叶化、使用、卸载 7个等级。其中
验证、准备、解析 那多少个解析被誉为连接进度。

诚如的话,以上进程会鲁人持竿地从头,但只是开头,因为那个等级会陆续举行,日常会在一个品级早先后激活调用另一个经过。

以下4种状态下,一定会开始类的加载进程:

  • 相遇new、getstatic、putstatic、invokestatic等字节码指令时,而类没有通过初始化。生成这4个指令的广阔景况:使用new关键字实例化对象、读取或设置类的静态字段(使用final关键字修饰除外)、调用类的静态方法

  • 动用java.lang.reflect举行反射调用,而类没有经过伊始化。

  • 那阵子始化一个类时,发现其父类还不曾被早先化,则要求先开首化其父类

  • 当虚拟机启动时,用户须要指定一个主类(包涵main方法),虚拟机会初步化这一个主类。

回看第1条,为何final关键字修饰的静态成员变量会分歧呢?回想Class文件结构常量池知识,如若是final关键字修饰的static变量,它的值使用ConstantValue举行初叶化,非final修饰的static变量在
clinit 方法中初阶化。

上述4种现象被虚拟机称为有且唯有的会触发类早先化的现象,那4种意况被叫作对类的能动引用。除此之外的兼具场景都不会触发类的开始化,被称作被动引用。

public class SuperClass {
    /*
     * 被动引用父类静态变量,不会初始化子类
     */
    static{
        System.out.println("super class init");
    }
    public static int value = 123;
}

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

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

/*
 * 被动引用父类静态变量,不会初始化子类
 */
public static void invokeSuperStatic(){
    System.out.println(SubClass.value);
}

/*
 * 通过数组定义来引用类,不会触发类的初始化
 */
public static void accessByArray(){
    SuperClass[] array = new SuperClass[10];
}

/*
 * 访问final static变量,不会初始化类
 */
public static void accessFinalField(){
    System.out.println(ConstClass.HELLO);
}

如上代码,一共对应了3种被动引用格局。

  • 引用父类的静态变量,不会伊始化子类
  • 因此数组定义来引用类,不会触发类的初始化。此时髦无开头化 SuperClass
    类,但起头化了 [Lcom.okunu.jvm.init
    那么些类,它由字节码指令newarray触发,且完结了数组的 length
    等艺术。数组也是目标,是Java自动生成的目的,所以它并不会去开始化数组中的元素类。
  • static
    final类型的常量,在编译阶段已把此常量存储到了NotInit类的常量池了,对常量
    HELLO
    的引用已经转向对自家常量池的引用了,所以不会初阶化ConstClass类了

类加载进度

类加载进程分成 加载、验证、准备、解析、起头化、使用、卸载
7个级次,本文中助教前面5个阶段

1、加载

加载阶段重点做以下工作:

  • 经过类的全限定名获取类的二进制文件流
  • 将字节流所表示的静态存储结构变化为方法区的运行时数据结构
  • 在堆中生成此类的 java.lang.Class 对象,作为方法区这几个数量的拜访入口

加载阶段是支付可控性最强的等级,比如说开发可以运用自定义类加载器去加载某个类,类的发源也足以是jar包、class文件、甚至是网络流。

2、验证

表明是三番五次进度的第1步,它是为着保证Class文件的字节流符合虚拟机的专业。

加载阶段加载Class文件,并未确定Class文件的根源,要是愿意,甚至可以手动编写Class文件,但如此的Class文件有可能不切合虚拟机规范,所以要求声明。它主要进行如下方面证实:

  • 文件格式验证
  • 元数据声明
  • 字节码验证
  • 标记引用验证

3、准备

准备阶段是为类变量正式分配内存并赋默许值的级差,那几个内存都在方法区内分配。

有三个点须求强调:

  • 只为类变量分配内存,即是为static变量分配内存,一般成员变量是在类被实例化时分配内存
  • 只会为static变量赋默许值

如若有如下static变量

  public static int value = 123

那么准备阶段,value的值会变成0,而不是123。而把value赋值为123的putstatic指令是在程序被编译后,存放在
clinit 方法中,所以value被赋值为123 爆发在 初步化 阶段,即第5个级次。

一旦是final修饰的static变量呢?借使字段的字段属性表中设有ConstantValue属性,那么准备阶段,变量就会被起首化为ConstantValue存储的值。

只要上述变量为

  public static final int value = 123

编译时javac会为value生成ConstantValue属性,准备阶段value的值就会变成123。

4、解析

解析阶段是将常量池中的符号引用替换为直接引用的历程。

标记引用是指字面量,能无歧义地定位到对象就好,它与虚拟机的内存布局无关。

一贯引用,是指可以一贯访问到目的的指针或句柄,与内存布局相关的。

浅析主要针对类或接口、字段、类情势、接口方法4类符号引用。

5、初始化

初始化是类加载进程的尾声一步,除了加载进度,开发可以运用自定义类加载器加入外,其他进度全都是虚拟机自己支配着。开首化阶段,才真的先河施行Java程序代码。

准备阶段,为static变量分配内存并赋默许值,而起首化阶段则会执行clinit方法,为static变量分配代码中所指定的值。

  • <clinit>方法是虚拟机自动生成的,编译器收集类中类变量的赋值语句、static静态语句块合并发生<clinit>方法。收集的依次是代码顺序

  • <clinit>方法和 init
    方法不一样,它不需求突显调用父类<clinit>方法,因为虚拟机有限支撑在调用子类<clinit>方法前,父类<clinit>方法已经履行已毕。

  • 因为父类的<clinit>方法先进行,所以父类静态语句块要事先于子类的静态语句块。

  • <clinit>方法并不是必须的,借使代码中没有定义静态变量或者静态语句块,则并未<clinit>方法。

类加载器

类加载器的功用就是完结加载阶段的天职,通过一个类的全限定名获取Class文件的二进制字节流。

假就像是一个类由八个例外的类加载器加载,得到八个实例,那么那两个实例在虚拟机看来,并不雷同连串型。

类加载器可以分成三类:

  • 开行类加载器(Bootstrap ClassLoader),负责将存放在 JAVA_HOME\lib
    目录下同时是编造机识其余类库加载到虚拟机内存当中来

  • 扩展类加载器(Extension ClassLoader),它承担加载存放在
    JAVA_HOME\lib\ext 目录下的类库

  • 用户程序类加载器(Application
    ClassLoader),它是默许的类加载器,负责加载ClassPath上点名的类库,如若某个类没有卓殊指定类加载器,就利用它。

我们的应用程序都是由那三类类加载器完结类加载的,借使要求,大家仍是可以完毕自定义的类加载器达成类加载。它们的涉嫌如下图所示:

上图所出示的类加载器关系,就叫类加载器的家长委派模型。除了顶层的开行类加载器,其余的类加载器都有自己的父类加载器,但它们不是透过一连完毕的,而是经过整合落成的。

大人委派模型的工作历程是:假如一个类加载器收到了类加载请求,会先请求自己的父类加载器去加载,要是父类无法加载该类,子类才会尝试自己去加载。

protected Class<?> loadClass(String name, boolean resolve){
    Class c = findLoadedClass(name);
    if (c == null) {
        if (parent != null) {
            //使用父加载器加载此类
            c = parent.loadClass(name, false);
        } 
        if (c == null) {
            // 如果父加载器没有成功加载,则自己尝试加载
            c = findClass(name);
        }
    }
    return c;
}

翻看ClassLoader类的loadClass方法,双亲委派模型从上述代码中贯彻。

行使双亲委派模型,Java类和它的类加载器一起怀有了一种含有优先级的层系关系。例如Object类,无论哪个类加载器来加载它,最终都要委托启动类加载器来加载Object,那就有限支撑了Object在先后的次第类加载器环境中都是同一个类。Java的基本类,基础行为不可不被担保,无法被曲解,这就是父母委派模型的意义所在。

相关文章