JVM的类加载机制

一、基本概念

JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。

1. 加载

加载是类加载过程中的一个阶段,这个阶段虚拟机要完成3件事。

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)

2. 验证

这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证主要包含4个阶段的校验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

3. 准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:

public static int value = 123;

实际上变量 value 在准备阶段过后的初始值为 0 而不是 123,将 value 赋值为 123 的 putstatic 指令是程序被编译后,存放于类构造器方法之中。
但是注意如果声明为:

public static final int value = 123;

在编译阶段会为 value 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 value 赋值为 123。

4. 解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是在 class 文件中以: CONSTANT_Class_info、CONSTANT_Field_info
、CONSTANT_Method_info等类型的常量出现。

5. 初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。

那么,什么时候开始初始化?

  1. 使用 new 该类实例化对象的时候;
  2. 读取或设置类静态字段的时候(但被final修饰的字段,在编译器时就被放入常量池的静态字段除外static final);
  3. 调用类静态方法的时候;
  4. 使用反射 Class.forName(“xxxx”) 对类进行反射调用的时候,该类需要初始化;
  5. 初始化一个类的时候,有父类,先初始化父类(注:1. 接口除外,父接口在调用的时候才会被初始化;2.子类引用父类静态字段,只会引发父类初始化);
  6. 被标明为启动类的类(即包含main()方法的类)要初始化;
  7. 当使用 JDK1.7 的动态语言支持时,如果一个 java.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上情况称为对一个类进行主动引用,且有且只要以上几种情况需要对类进行初始化。

二、总结

当使用 Java 命令运行 Java 程序时,此时 JVM 启动,并去方法区下找 Java 命令后面跟的类是否存在,如果不存在,则把类加载到方法区下(还记得运行时数据区域里的方法区吗?)

在类加载到方法区时,会分为两部分:

  1. 先加载非静态内容到方法区下的非静态区域内,再加载静态内容到方法区下的静态区域内。
  2. 当非静态内容载完成之后,就会加载所有的静态内容到方法区下的静态区域内。

上边这两部分概括为如下五步:

  1. 先把所有的静态内容加载到静态区域下
  2. 所有静态内容加载完之后,对所有的静态成员变量进行默认初始化
  3. 当所有的静态成员变量默认初始化完成之后,再对所有的静态成员变量显式初始化
  4. 当所有的静态成员变量显式初始化完成之后,JVM 自动执行静态代码块(静态代码块在栈中执行)[如果有多个静态代码,执行的顺序是按照代码书写的先后顺序执行]
  5. 所有的静态代码块执行完成之后,此时类的加载完成

巴拉巴拉,一堆理论,不够形象,补张图

JVM的类加载机制

三、练一练

如下是一道真实的面试题,希望上文的理论部分对你有所帮助!

class Singleton{

    private static Singleton singleton = new Singleton();
    public static int value1;
    public static int value2 = 0;

    private Singleton(){
        value1++;
        value2++;
    }

    public static Singleton getInstance(){
        return singleton;
    }

}

class Singleton2{
    public static int value1;
    public static int value2 = 0;
    private static Singleton2 singleton2 = new Singleton2();

    private Singleton2(){
        value1++;
        value2++;
    }

    public static Singleton2 getInstance2(){
        return singleton2;
    }

}

public static void main(String[] args) {

        Singleton singleton = Singleton.getInstance();
        System.out.println("Singleton1 value1:" + singleton.value1);
        System.out.println("Singleton1 value2:" + singleton.value2);

        Singleton2 singleton2 = Singleton2.getInstance2();
        System.out.println("Singleton2 value1:" + singleton2.value1);
        System.out.println("Singleton2 value2:" + singleton2.value2);

}

说出运行的结果:
Singleton1 value1 : 1
Singleton1 value2 : 0
Singleton2 value1 : 1
Singleton2 value2 : 1

Singleton输出结果:1 0 原因:
  1. 首先执行main中的Singleton singleton = Singleton.getInstance();
  2. 类的加载:加载类Singleton
  3. 类的验证
  4. 类的准备:为静态变量分配内存,设置默认值。这里为singleton(引用类型)设置为null,value1,value2(基本数据类型)设置默认值0
  5. 类的初始化(按照赋值语句进行修改):
    执行private static Singleton singleton = new Singleton();
    执行Singleton的构造器:value1++;value2++; 此时value1,value2均等于1
    执行
    public static int value1;
    public static int value2 = 0;
    此时value1=1,value2=0
Singleton2输出结果:1 1 原因:
  1. 首先执行main中的Singleton2 singleton2 = Singleton2.getInstance2();
  2. 类的加载:加载类Singleton2
  3. 类的验证
  4. 类的准备:为静态变量分配内存,设置默认值。这里为value1,value2(基本数据类型)设置默认值0,singleton2(引用类型)设置为null,
  5. 类的初始化(按照赋值语句进行修改):
    执行
    public static int value2 = 0;
    此时value2=0(value1不变,依然是0);
    执行
    private static Singleton singleton = new Singleton();
    执行Singleton2的构造器:value1++;value2++;
    此时value1,value2均等于1,即为最后结果
上一篇:Keepalived+Nginx解决方案实现高可用的API网关(nginx)


下一篇:Linux进阶(一)