Java类加载机制
Java类加载机制
类是在运行期间第一次使用时动态加载的,而不是编译时期一次性加载。因为如果在编译时期一次性加载,那么会占用很多的内存。
类的生命周期
包括以下 7 个阶段:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
类加载过程
包含了加载、验证、准备、解析和初始化这 5 个阶段。
1. 加载
加载过程完成以下三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构
- 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口
2. 验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3. 准备
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在堆中。
注:实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
public static int value = 123;
如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。
public static final int value = 123;
4. 解析
将常量池的符号引用替换为直接引用的过程。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
5. 初始化:为类的静态变量赋初值
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。
clinit 与 init:
在编译生成class文件时,编译器会产生两个方法在class文件中,一个是类的初始化方法clinit, 另一个是实例的初始化方法init。
clinit指的是类构造器,主要作用是在类加载过程中的初始化阶段进行执行,执行内容包括静态变量初始化和静态块的执行。
- static变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定。静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
-
虚拟机会自动保证在执行clinit方法时,必须先执行父类的clinit方法。虚拟机中第一个执行clinit方法的类肯定是 java.lang.Object
-
由于父类的clinit方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 2
}
-
如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成
-
执行接口的clinit方法不需要先执行父接口的clinit方法。只有当父接口中定义的变量使用时,父接口才会初始化
接口的实现类在初始化时也一样不会执行接口的clinit方法 -
虚拟机会保证一个类的clinit方法只执行一次,在多线程环境下被正确的加锁和同步。如果多个线程同时初始化一个类,只会有一个线程执行这个类的clinit方法,其它线程都会阻塞等待,直到活动线程执行clinit方法完毕
类初始化时机
1. 主动引用
虚拟机规范中严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
-
遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化
最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段的时候;调用一个类的静态方法的时候 -
使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化
-
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类
-
当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
2. 被动引用
除以上 5 种场景中的行为,所有引用类的方式都不会触发初始化。
被动引用的常见例子包括:
- 通过子类引用父类的静态字段,不会导致子类初始化
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法
SuperClass[] sca = new SuperClass[10];
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
System.out.println(ConstClass.HELLOWORLD);
类与类加载器
两个类相等需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、 isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
类加载器分类
JVM 提供了 3 种类加载器:
-
启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类 -
扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库 -
应用程序类加载器(Application ClassLoader)
负责加载用户路径(classpath)上的类库。JVM通过双亲委派模型进行类的加载,也可以通过继承 java.lang.ClassLoader实现自定义的类加载器
双亲委派模型
如果一个类加载器收到了类加载器的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成。
每个层次的类加载器都是如此,因此所有的加载请求最终都会传送到Bootstrap类加载器(启动类加载器)中。
只有父类加载反馈自己无法加载这个请求(在它的加载路径下没有找到所需加载的Class)时,子加载器才会尝试自己去加载。
好处
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。