在《Head First Java》和《Thinking In Java》中,分章节逐步讲了一个完整的 Java 类是如何完成初始化工作的。我在以前的笔记中,也做了一定的总结。但是因为分散在不同的笔记中,有时候混淆了查找起来就不是很方便。所以,决定将这块抽出来完整的讲一下。
一个类正常的使用过程大概是这样的:
- 触发了 static 关键字、new、reflect,要使用到某个类
- JVM 需要将.class 文件加载进来
- 连接过程中的准备阶段
- 初始化
- 是否有 new?
下面我们就来详细说一下这几个步骤:
####1. 触发
当在程序中使用了静态的方法或者字段、new 了一个对象、使用反射,就意味着要使用这个类。而因为 Java 是动态加载机制,就要再使用到的时候再将.class 文件加载进 JVM 中。所以,触发类的初始化条件有且只有:
- 遇到 new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化
- 使用 java.lang.reflect 包的方法对类进行反射调用时
- 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那么类),虚拟机先初始化这个主类
触发之后,就会通知 JVM:“嗨,哥们。赶紧把我要用的类拉进来啊!”
####2. 加载
然后,JVM 会加载这个类。加载过程其实就是通过类加载器工作的,这个在前面文章中详细讲过,如果忘了可以看一下。
####3. 准备
在类的加载过程中,一共会有5个大步骤:
- 加载
- 连接
- 初始化
- 使用
- 卸载
而第一次涉及到类的初始化,99%的情况都会发生在准备阶段。另外的1%,第一次是在编译时。条件是static final
。
假如变量是由
static final
修饰的,在编译时,会将这个变量标记为 ConstantValue 属性。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这里需要强调一下:
- 这时候进行的内存分配仅仅包含类变量,而不包括实例变量,因为实例变量将会在堆上分配
- 这里说的初始值指的是数据类型的零值,比如
public static int value = 100;
,在准备阶段之后值就是0而不是100,因为将 value 设置为100的 putstatic 命令存放在类构造器<clinit>()
方法中,而这个方法是在初始化阶段才会执行的
####4. 初始化
上面的准备阶段是针对方法区的类属性而言,而到了初始化阶段,才真正开始执行类中由程序员定义的代码(本质上是翻译之后的字节码)。而这一切是通过<clinit>()
方法来执行的:
<clinit>()
是由编译器自动收集类中的所有类变量的赋值语句和静态语句块中的语句合并而成的,而在这个方法中的顺序就是程序员自己在类中定义的顺序<clinit>()
与类的构造函数(()方法)不同,它不需要显式的调用父类构造器,虚拟机会保证在子类的` ()`执行前,父类的` ()`已经被执行过。所以,在虚拟机中第一个被执行的` ()`肯定是 java.lang.Object 的 <clinit>()
是线程安全的- 由于父类的
<clinit>()
方法先执行,那么父类的所有赋值操作(包括静态语句块)都早于子类的变量赋值操作。下面是例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static class Parent {
public static int A = 1;
static { //优于 Sub 的赋值操作
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}/*output:
2
*/
####5. 是否 new 了新对象?
到这里,一个类已经被加载进内存了。
如果没有调用 new 创建一个对象而是仅仅用到静态属性,这个过程就 over 了。如果还调用了 new 创建对象,就会在堆上为堆上分配内存,并且对类的属性进行一次初始化。初始化的值也是数据类型的零值。如果类的属性被初始化(C++不允许这样做),那么这时候就会再进行一次属性初始化。
最后一步,执行构造函数。