JVM - ClassLoader装载流程

注意: 装载加载 的区别:java

  • 装载,指的是.class文件加载到初始化的整个生命周期;
  • 加载,指的是.class文件装载的第一个阶段

类装载机制

jvm把class文件加载到内存中,并对数据进行校验、解析和初始化,最终造成JVM能够直接使用的java类的全过程。安全

ClassLoader的装载流程图

输入图片说明

类装载的前提条件

class只有使用的时候才会被装载,java虚拟机也不会无条件的装载class类型数据结构

装载流程

  1. 加载类

加载类处于类装载的第一个阶段,将class文件的字节码加载到内存中,并将静态数据转换成方法区中的运行时数据结构,在堆中生成一个表明这个类的java.lang.Class对象,做为方法区类数据的访问入口。jvm

该过程须要ClassLoader参与。布局

加载类,JVM必须完成:测试

  • 经过类的全名,获取类的二进制数据流
  • 解析类的二进制数据流为方法区内的数据结构
  • 建立java.lang.Class类的实例,表示该类型
  1. 链接

将java类的二进制代码合并到JVM的运行状态中。这一步包含三个操做:优化

  • 验证,确保加载的类信息符合JVM规范,没有安全方面的问题
  • 准备 验证经过后,虚拟机就会进入准备阶段,在这个阶段,虚拟机会为这个类变量在方法区分配相应的内存空间,并设置初始值。下图为虚拟机为各类类型变量默认的初始值:

输入图片说明

注意:线程

  • java并不支持boolean类型,对于boolean类型,内部实现是Int,因为int的默认值是0,故对应的boolean默认值是false。code

  • 此处进行内存分配的只是类变量(static修饰的变量),不包括实例变量(实例变量会在对象实例化时随着对象一块儿分配在java堆中)对象

  • 解析,该阶段的任务就是将类、接口、字段和方法的符号引用转为直接引用

符号应用,就是一些字面量的引用,和虚拟机的内部数据结构和内存布局无关。

  1. 初始化类

初始化是类装载的最后一个阶段。若是前面的操做没有问题,表示类能够顺利装载到系统中。此时,类才会开始执行java字节码。

该阶段的重要工做,是执行类的初始化方法<clinit>, 为类变量赋予正确的值。方法<clinit>是由编译器自动生成的,它是由类静态成员的赋值语句以及static语句块合并产生的。

在加载一个类以前,虚拟机老是会试图加载该类的父类,所以父类的<clinit>方法老是在子类<clinit>以前被调用。

  1. 初始化一个类包含两个步骤:
  • 若是类存在超类,先初始化超类
  • 若是类存在类初始化方法,就执行此方法
  1. 初始化一个接口只有一个步骤: 若是该接口存在接口初始化方法,就执行此方法,接口不初始化父接口。

注意

  • java编译器并非为全部的类都产生一个<clinit>初始化方法,如下几种状况就没有: ① 类没有申明类变量,也没有任何静态初始化语句(static代码块); ② 类申明了类变量,可是没有任何的类变量初始化语句,也没有静态初始化语句进行初始化; ③ 类仅包含静态final变量的类变量初始化语句,并且是编译时候的常量
  • 初始化类的过程必须保持同步,若是有多个线程初始化一个类,仅仅容许一个线程执行初始化,其余的线程都须要等待。。

类初始化

JVM规定:一个类或者接口在初次使用时,必须进行初始化。这里的“使用”,指的是”主动使用”,包括如下几种状况:

  • 当建立一个类的实例时,好比使用new关键字,或者经过反射、克隆、反序列化
  • 当调用类的静态方法时,即当使用了字节码invoke static指令
  • 当使用类或接口的静态字段时(final常量除外),即便用了getstatic或putstatic指令
  • 当使用java.lang.reflect包中的方法反射类的方法时
  • 当初始化子类时,必须先初始化父类
  • 做为启动虚拟机、含有main方法的那个类

除了以上状况属于主动使用外,其余均属于被动使用,被动使用不会引发类的初始化。

主动使用示例

public class Parent {
    static {
        System.out.println("Parent init.");
    }
}

public class Child extends Parent {
    static {
        System.out.println("Child init.");
    }
}

public class InitMain {
    public static void main(String[] args) {
        Child child = new Child();
    }
}

上述示例声明了三个类:Parent、Child(extends Parent)、InitMain。若Parent被初始化,static被执行将打印“Parent Init”;若Child被初始化,将会打印“Parent init”、"Child init"。执行InitMain,打印结果为:

Parent init.
Child init.

总结:

根据上述示例可知,系统首先加载Parent类,接着装载Child类。符合主动装载中的两个条件,使用new关键字建立类的实例会装载相关的类,以及在初始化子类时,必选先初始化父类。

被动装载示例

public class Parent {
    static {
        System.out.println("Parent init");
    }

    public static int v = 100;
}

public class Child extends Parent {
    static {
        System.out.println("Child init.");
    }
}

public class InitMain {
    public static void main(String[] args) {
        System.out.println(Child.v);
    } 
}

说明:Parent类中定义了类变量v,在InitMain测试类中,使用子类Child调用父类中的类变量v。

执行结果:

Parent init
100

总结:

在InitMain测试类中,经过子类Child直接访问了Parent类中的static变量v,可是子类Child并未初始化,只有父类Parent完成初始化。因此,在引用一个字段时,只有直接定义该字段的类,才会被初始化

注意:虽然子类Child没有被初始化,可是此时Child类已经被系统加载,只是没有进入到初始化阶段

引用final常量

public class FinalFieldClass {
    public static final String CONST_STR = "hello world";
    
    static {
        System.out.println("FinalFieldClass init");
    }
}

public class FinalFieldTest {
    public static void main(String[] args) {
        System.out.println(FinalFieldClass.CONST_STR);
    }
}

运行结果: hello world.

分析:FinalFiledClass类没有由于其常量字段CONST_STR被引用而初始化,这是由于在Class文件生成时,final常量因为其不变性,作了适当的优化。

总结:

编译后的FinalFieldClass.class中,并无引用FinalFieldClass类,而是将其final常量直接存放在常量池中,所以FinalFiledClass类天然不会被加载。javac在编译时,将常量直接植入目标类,再也不使用被引用类。

注意:并非在代码中出现的类,就必定会被加载或者初始化,若是不符合主动使用的条件,类就不会被初始化。

相关文章
相关标签/搜索