本文参考《深入理解Java虚拟机》

主动使用和被动使用

Java语言处处存在懒加载思想,如HashMap和ArrayList的初始化,同样,所有的Java虚拟机实现必须在每个类或接口被Java程序首次主动使用时才初始化他们,当然现代JVM有可能根据程序的上下文语义推断出接下来可能初始化谁。

主动使用:

  1. new,直接使用;
  2. 访问某个类或者接口的静态变量,或对该静态变量进行赋值操作;
  3. 调用静态方法;
  4. 反射某个类;
  5. 初始化一个子类;
  6. 启动类,比如java HelloWorld。

除了以上六个,其余的都是被动使用,不会导致类的初始化。

被动使用

  1. 通过子类引用父类的静态变量只会导致父类初始化。
  2. 引用数组类型:new某个类的数组,不会引起类的初始化。
  3. 引用常量类型时,
    编译器能直接算出来的就不会初始化(如 :public static final int x = 1),
    需要算的就会初始化(如 :public static final int x = new Random().nextInt(100))。
    就是说final修饰的复杂类型无法在编译期算出的,会引起初始化类。

一个小坑
对于如下代码,问x,y在类初始化完成后的值。

public static int x = 0;
public static int y;

private static Singleton instance = new Singleton();

private Singleton() {
    x++;
    y++;
}
private static Singleton instance = new Singleton();

public static int x = 0;
public static int y;

private Singleton() {
    x++;
    y++;
}

注意两段代码由于instance位置不同,在变量x和y前面定义的时候会引起Singleton方法运行,导致只完成准备阶段的x和y都由0变为一,但是程序继续往下运行到x和y赋值,x又被初始化为正确的值,即0,而y由于没有赋值因此还是1。

在变量x和y后面定义的时候,x和y都完成了初始化,因此都可以正确的++,结果为x=1,y=1。

类的加载过程

加载

类的加载的最终产品是位于堆中的Class对象。
Java类加载过程
在加载阶段,JVM需要完成以下三件事:

  1. 通过一个类的全限定名来获取此类的二进制字节流;
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在堆中生成这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

连接

连接阶段的部分内容(如一部分字节码文件格式验证动作)和加载阶段会交叉进行,即加载阶段尚未完成,连接阶段就已开始。

验证

主要为了确保Class文件中的字节流中包含的信息符合虚拟机的要求且不会损害JVM自身的安全。主要包含以下4个动作:

  1. 文件格式验证:字节流是否符合Class文件格式规范,并且能被当前版本虚拟机处理。
    1. 是否以魔数0xCAFEBABE开头;
    2. 主、次版本号是否在当前JVM处理范围内;
    3. 常量池中的常量类型是不是都支持。
    4. etc.
  2. 元数据验证
    1. 这个类是否有父类(除了java.lang.Object,其他类都应该有父类);
    2. 父类是否允许继承;
    3. 是否实现了抽象方法;
    4. 是否覆盖了父类的final字段;
    5. 其他语义检查。
  3. 字节码验证
    主要进行数据流和控制流的分析,确定程序语义是合法、符合逻辑的。即不会出现这样的情况,在操作栈中放置了一个int类型,却给了一个long型数据。
  4. 符号引用验证
    发生在JVM将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段——解析阶段中发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常包含以下内容:
    1. 符号引用中通过字符串描述的全限定类名是否能找到对应的类;
    2. 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;
    3. 符号引用中的类、方法、字段的访问性(public、private、protected)是否可被当前类访问。
    4. etc.

符号验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,将会抛出一个java.lang.IncompatibleClassChangeError异常的子类,比如Java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSuchMethodError。
可以使用启动参数来关闭大部分类验证措施,缩短虚拟机类加载时间。

准备

准备阶段就是为类的变量正式分配内存并设置初始值。这个初始值与初始化不是同一个概念。

比如;

public static int value = 12;

这个阶段value的值为0 而不是12。value赋值为12的阶段是在初始化的过程中出现的。

Java所有的基本类型都赋值为零值。(简单来说就是0 or null,0.0f,false等)

总结:类的属性是会默认初始值的。而局部变量没有初始值。所以是未定义的。

准备阶段各基本类型的赋值如下表:
图片说明

解析

包含以下四个方面:

  1. 类或者接口的解析;
  2. 字段解析;
  3. 类方法解析;
  4. 接口方法解析。

虚拟机规范之中并未规定解析阶段发生的具体时间,只要求了在执行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield和putstatic这16个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

初始化

只有主动引用才会导致类的初始化哦!

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达,初始化阶段是执行类构造器<clinit>()方法的过程

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

<clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object,由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作

<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步(天生线程安全),如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。

需要注意的点小总结:
1. <clinit>()方法对于一个类来说不是必须的;
2. 接口中也存在<clinit>()方法;
3. 虚拟机有义务把证<clinit>()方法的线程安全。

初始化阶段很容易出成面试问题,可以参考 https://blog.csdn.net/weixin_33860528/article/details/86026350 中的父类子类初始化的输出问题。

类加载器

Java中的class文件是通过类的加载器装载到JVM中的。
Java默认有三种类加载器:
图片说明

各个加载器的作用:
1)Bootstrap ClassLoader:负责加载JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
2)Extension ClassLoader:负责加载Java平台中扩展功能的一些jar包,包括JAVA_HOME中jre/lib/ext/*.jar或-Djava.ext.dirs指定目录下的jar包;
3)Appication ClassLoader:负责记载classpath中指定的jar包及目录中class.

工作流程:
1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成;
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成;
3、如果BootStrapClassLoader加载失败(例如在JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载;
5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

这就是所谓的双亲委派模型。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。

好处:防止内存中出现多份同样的字节码(安全性角度)。
PS:类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。