JVM - ClassLoader装载流程

注意: 装载加载 的区别:

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

类装载机制

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

ClassLoader的装载流程图

输入图片说明

类装载的前提条件

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

装载流程

  1. 加载类

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

该过程需要ClassLoader参与。

加载类,JVM必须完成:

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

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

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

输入图片说明

注意:

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

  • 此处进行内存分配的只是类变量(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在编译时,将常量直接植入目标类,不再使用被引用类。

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

猜你喜欢

转载自my.oschina.net/u/1251536/blog/1620354