虚拟机类加载机制

# 类加载时机

一个类型从被加载到虚拟机内存开始,到卸载出内存为止,它一共会经历加载、验证、准备、解析、初始化、使用、卸载这七个阶段。

# 1.加载

Java虚拟机规范并没有强制约束加载的时机,但是对于初始化而言,加载、验证、准备需要在此之前完成。

  • 遇到new getstatic putstatic invokestatic字节码指令时(分别对应创建对象,读取或者改变静态变量,执行静态方法)
  • 使用java.lang.reflect包对类型进行反射调用
  • 初始化类之前必须初始化父类
  • 虚拟机启动,用户必须指定一个要执行的主类(包含main方法) ... 虚拟机在加载阶段需要完成以下三件事
  1. 通过类的全限定名来获取此类的字节流
  2. 字节流中的静态存储结构转换为方法区的运行时数据结构
  3. 内存中生成java.lang.Class对象代表此字节流,作为方法区这个类各种数据访问入口

虚拟机规范并没有规定一定需要从.class文件加载字节流,从zip压缩包中获取,成为jar和war格式的基础;运行时计算生成,使用最多的就是动态代理技术。
需要注意的是数组类不同于普通的类,它是由Java虚拟机直接在内存中动态构造出来的,但数组类的元素类型最终还是靠类加载器来完成加载。

# 2.验证

确保Class文件的字节流中包含的信息符合Java虚拟机规范的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。 验证主要包含以下四个动作

  1. 文件格式 魔数、主次版本号、常量池中的常量等等
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

# 3.准备

正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。这些变量所使用的内存都应当在方法区中分配, 但是不同的虚拟机以及不同的JDK版本,方法区的具体实现也不同。需要注意的是准备阶段赋初始值是数据类型的零值

public static int a = 112;
1

初始阶段会设置a为int类型的0,因为这个时间还没有执行任何java方法,112在编译后,存在在类构造器clinit方法中,什么时候a=112呢, 需要等到类的初始化阶段才会被执行。特殊情况,当类变量同时有final修饰时,虚拟机在准备阶段就会赋值112.

# 4.解析

虚拟机将常量池内的符号引用替换为直接引用的过程,直接引用与虚拟机实现的内存布局直接相关,而符号引用。 虚拟机规范并没有对解析发生的具体时间,可以在加载时就对常量池中的符号引用进行解析,也可以等到一个 符号将要被使用前采取解析。解析阶段类或者接口的解析、字段解析、方法解析,可能会抛出java.lang.NoSuchFieldError和java.lang.NoSuchMethodError

# 5.初始化

直到初始化阶段,Java虚拟机才真正开始执行类中编译的Java代码,把主导权交给应用程序。在准备阶段,类变量已经赋值过初始零值,在初始化阶段, 会根据程序员意愿去初始化类变量和其他资源。初始化阶段就是执行类构造器clinit方法的过程,clinit方法并不是程序员在java代码中直接编写的方法, 它是javac编译器的自动生成物。
  clinit方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的, 静态语句块只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
  clinit和init方法区别,clinit不需要显式的调用父类构造器,虚拟机会保证父类的clinit方法会在子类clinit方法之前执行,所以Java虚拟机中 第一个被执行的clinit方法的类型肯定是java.lang.Object.这解释了为什么父类的静态代码块比子类静态码块先执行。
  接口不能使用静态代码块,但是也存在变量初始化操作,因此接口和类一样,虚拟机同样会生成clinit方法,但接口和类不同的是,实现接口的类或者继承接口的接口, 不需要执行父接口的clinit方法,只有在使用到父接口中定义的变量时,父接口才会被初始化。另外需要注意多线程环境下,如果再静态块有耗时很长的操作,同时有多个方法执行初始化过程, 可能会导致多个线程阻塞。

public class DeadLoopClass {

    static{
        if (true) {
            System.out.println(Thread.currentThread().getName() + "clinit");
            while (true) {

            }
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
    Runnable runnable = () -> {
        System.out.println(Thread.currentThread().getName() + "start");
        new DeadLoopClass();
        System.out.println(Thread.currentThread().getName() + "end");
    };


    new Thread(runnable, "下单").start();
    new Thread(runnable, "退单").start();
}
1
2
3
4
5
6
7
8
9
10
11

其他线程一直不能执行静态代码块,也就是clinit方法,必须等拿到锁的线程执行完毕。这里需要注意,其他线程虽然会被阻塞,但是如果执行clinit方法线程退出clinit方法,其他线程 唤醒后不会再进入clinit方法,同一个类加载器下,一个类型只会被初始化一次。

# 6.使用

# 7.卸载