类的加载、连接、初始化
当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三个步骤对该类进行初始化,通常情况下JVM会连续完成这三个步骤。
1. 类的加载
单纯的类加载指的是将类的class
文件读入内存,并为之创建一个java.lang.Class
对象。
- 这意味着,当程序中使用任何类时,系统都会为之建立一个
java.lang.Class
对象。 - 实际上,每个类是一批具有相同特征的对象的抽象,而系统中所有的类实际上也是实例,它们都是
java.lang.Class
的实例(老哲学人了)。
类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader
基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:
- 从本地文件系统加载
class
文件:绝大多数情况; - 从JAR包加载
class
文件:也很常见,JDBC编程时用到的数据库驱动类就放在JAR文件中; - 通过网络加载
class
文件。
类加载器通常无须等到使用该类时才加载该类,JVM 规范允许系统预先加载某些类。
2. 类的连接
当类被加载,系统为之生成一个对应的Class
对象后,会进入连接阶段,连接阶段负责把类的二进制数据合并到 JRE 中。类连接又分为三个阶段:
- 验证:检验被加载的类是否有正确的内部结构,并和其他类协调一致;
- 准备:为类的类变量分配内存,并设置默认初始值;
- 解析:将类的二进制数据中的符号引用替换成直接引用(?)。
3. 类的初始化
在类的初始化阶段,JVM负责对类进行初始化,主要就是对类变量进行初始化。
3.1 复习
当然,要记得,在Java中对类变量指定初始值有两种方式:①声明类变量时指定初始值;②使用静态初始化块为类变量指定初始值。
class Test1 {
static int a = 5; // 声明变量a时指定初始值
static int b;
static int c;
static {
b = 6; // 使用静态初始化块为变量b指定初始值
}
// ...
// a, b, c 的值分别为 5, 6, 0
}
class Test2 {
/*
* 声明变量时指定初始值、静态初始化块都被当成类的初始化语句
* JVM会按这些语句在程序中的排列顺序依次执行它们
*/
static {
b = 6; // 使用静态初始化块为变量b指定初始值
System.out.println("----------");
}
static int a = 5;
static int b = 9;
static int c;
public static void main(String[] args) {
System.out.println(Test2.b);
/*
* 输出结果为:
* ----------
* 9
*/
}
}
注意,上述Test2
的例子中在static
初始化块中出现的b = 6;
语句,该语句看似是在未声明变量b
时即给它赋值,但我们要理解:JVM在第二步类的连接阶段做的便是首先创建类的变量并赋默认值,而后才会发生第三步的类的初始化,执行相应的语句。因而尽管该static
初始化块位于代码最开头,JVM解析类的结构时也不是最先执行它的。
3.2 JVM初始化一个类的步骤
- 若该类的直接父类还未被初始化,先初始化其直接父类;
- 若该类中有初始化语句,则系统依次执行这些初始化语句。
3.3 类初始化的时机
当Java程序首次通过下面几种方式来使用某个类和接口时,系统就会初始化该类或接口。
- 创建类的实例:使用
new
操作符来创建、通过反射来创建; - 调用某个类/接口的
static
方法,或访问/赋值它们的static
变量; - 使用反射方式来强制创建某个类或接口对应的
java.lang.Class
对象; - 初始化某个类的子类(因为该子类的所有父类将被初始化)。
3.4 宏变量
值得注意的是,对于一个final
型的 static
变量,若该变量的值在编译时就可以确定下来,那么这个类变量相当于宏变量,Java编译器会在编译时直接把该类变量出现的地方替换成它的值,因此即使程序使用该静态类变量,也不会导致该类的初始化。
4. 其他
当使用ClassLoader
类的loadClass()
方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用Class
的forName()
静态方法才会导致强制初始化该类。