深入理解Java虚拟机(九)——类加载的概述和时机

1. 概述

虚拟机把描述类数据的Class文件加载到内存,经过验证、准备、解析、初始后形成一个能被直接使用的Java类型,这个过程就是虚拟机的类加载机制。

Java语言的类加载、连接、初始化是在运行期进行的,和在编译器就完成这些步骤的其它语言相比,Java的类加载策略会使得类加载时稍微增加了一些性能开销,但换来的好处是为Java程序提供高度的灵活性,这种灵活性体现在以下两方面:

  • ●面向接口面层的应用程序能在运行期指定实际的实现类。
  • ●可以自定义类加载器在运行期动态地加载网络或其他地方流进的二进制流,使之等位程序代码的一部分,

2. 类加载的时机

2.1 类加载顺序

类从加载到卸载的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。其中验证、准备、解析阶段可合称为连接。其中加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类加载过程就按照这种顺序开始加载,而解析阶段的顺序可能在初始化之前或之后开始。

2.2 类必须进行初始化的情况

Java虚拟机规范并没有说明何时进行类加载的第一个阶段:加载,但对于何时进行类加载过程的初始化化阶段,规范则做了严格的规定,当满足下列条件时,虚拟机必须进行类的初始化(类的加载、验证、准备阶段需要在初始化之前开始)。

  • 1.遇到new、getstatic、putstatic、invokestatic这4条字节码指令,而类又还没有进行初始化时。(注:这4条指令最常用的场景是:使用new关键字实例化对象、获取类的静态字段(final类型的静态字段除外)、设置类的静态字段、调用类的静态方法)
  • 2.使用反射技术加载类,而该类又还没有初始化过。
  • 3.初始化当前类,发现其父类还没有初始化时,需要先进行父类的初始化。
  • 4.用户指定的主类(就是调用main()方法的那个类)。

2.3 接口的初始化

接口的初始化和类的初始化稍有不同:当一个类被初始化时,要求其父类已初始化完毕;而当接口则不作此要求,即不要求父接口被初始化过,而是等到需要用到父接口时(例如引用父接口的常量)才对父接口进行初始化。

3. 类的被动引用

上述2.2小节的行为称之为对类进行主动引用,特点是会对类进行初始化。此外还有一种对类的被动引用,特点是引用类后并不会初始化该类。下面介绍几种被动引用的例子:

3.1 子类访问父类的静态字段,不会导致子类初始化

public class SuperClass {
	static {
		System.out.println("superClass init!");
	}
	public static int value = 123;
}
复制代码
public class SubClass extends SuperClass {
	static {
		System.out.print("subclass init!");
	}
}
复制代码
public class NotInit {
	public static void main(String[] args) {
		System.out.print(SubClass.value);
	}
}
复制代码

上述代码运行之后,不会输出“subclass init!”,只会输出"superClass init!"。对于静态字段的访问,只有直接定义该字段的类会被初始化。因此通过子类访问定义在父类中的静态字段,只会导致父类被初始化而子类不会初被始化。

3.2 通过数组定义来引用类,不会导致该类被初始化

public class NotInit {
	public static void main(String[] args) {
		SuperClass[] arr = new SuperClass[10];
	}
}
复制代码

上述代码运行之后不会不会输出“superclass init!”,说明该类没有被初始化。

3.3 当前类对其他类常量的引用,不会导致其他类的初始化。

public class ConstantClass {
	static {
		System.out.println("ConstantClass init!");
	}
	public static final int value = 10;
}
复制代码
public class NotInit {
	public static void main(String[] args) {
		System.out.println(ConstantClass.value);
	}
}
复制代码

上述代码执行后不会输出"ConstantClass init!"。当NotInit引用ConstantClass的常量时,后者的常量在编译阶段被转储到前者的常量池中,相当于前者自身已经拥有了该常量,因此不需要引用其他类的常量,也就不需要对其他类进行初始化。

猜你喜欢

转载自juejin.im/post/5d2830e36fb9a07eeb13d3f3