Java 类初始化顺序

这几天看java单例模式的时候,对类的加载机制和初始化顺序产生了疑问。于是在OSC发了一个技术贴关于java内部类加载顺序的问题,现在综合大家的回答总结下。


代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class SingletonFather {
//父类静态属性
public static SingletonFather testOutFather = new SingletonFather(1);
static {
System.out.println("Test Static-Father");
}
//父类构造函数
public SingletonFather(int i) {
System.out.println("Test " + i + " Construct!-Father ");
}
//父类静态方法
public static Singleton getInstance() {
return Inner.testInstance;
}
//父类静态内部类
private static class Inner {
public final static Singleton testInstance = new Singleton(3);
static {
System.out.println("TestInner Static-Father!");
}
}
}
public class Singleton extends SingletonFather {
//子类静态属性
public static Singleton testOut = new Singleton(1);
static {
System.out.println("Test Static");
}
//子类构造器
public Singleton(int i) {
super(i);
System.out.println("Test " + i + " Construct! ");
}
//子类静态方法
public static Singleton getInstance() {
return Inner.testInstance;
}
public static void main(String[] args) {
Singleton t = new Singleton(2);
Singleton.getInstance();
}
//子类静态内部类
private static class Inner {
public final static Singleton testInstance = new Singleton(3);
static {
System.out.println("TestInner Static!");
}
}
}

代码执行分析

先解释下这段代码:这里主要是有一个父类SingletonFather,它有一个静态内部类(里面包含一个静态属性testInstance和一个静态块)、一个带参数的构造方法SingletonFather()、一个静态属性testOutFather()和一个静态块。然后还有一个类Singleton,它继承了SingletonFather,里面包含的东西和SingletonFather是一样的,不过还有一个main方法,main方法里面new了一个Singleton,并调用了它的getInstance()静态方法。

ok,现在我们开始正题-类的加载机制。从java的语言哲学来看,一切都是对象。对jvm来说,一个类也是一个对象,它是特殊的对象,叫类对象。既然是对象,那必然有它的加载过程。而jvm对类的加载包括3个阶段:加载、链接、初始化。简单来说,加载阶段就是把放在硬盘里面的.class文件加载到内存中;链接阶段是链接一些接口、父类、元素类型等;而初始化就是根据执行类里面的一些属性和方法。在这里,对加载和链接不做深入研究。只研究类的初始化顺序。

现在,我们来看下上面代码的执行结果:

1
2
3
4
5
6
7
8
9
10
Test 1 Construct!-Father
Test Static-Father
Test 1 Construct!-Father
Test 1 Construct!
Test Static
Test 2 Construct!-Father
Test 2 Construct!
Test 3 Construct!-Father
Test 3 Construct!
TestInner Static!

我们来对这个输出结果分析下:
当jvm来到程序的入口函数main方法之前,它需要预先加载main方法的所在类,也就是Singleton类,而加载Singleton类,又需要先加载它的父类SingletonFather。所以,jvm最先初始化的类是SingletonFather。初始化的时候,会先按顺序执行它的静态块和静态属性。所以初始化SingletonFather的时候,会先执行静态属性

1
public static SingletonFather testOutFather = new SingletonFather(1);

这个时候输出

1
Test 1 Construct!-Father

然后按顺序执行到下一个语句,也就是static块

1
2
3
4
//父类类静态块
static {
System.out.println("Test Static-Father");
}

这时候输出

1
Test Static-Father

在这里我们需要注意的是父类的静态内部类Inner,因为我们并没有调用这个内部类,所以jvm并不会去初始化它。
然后,我们来到子类Singleton,同理,初始化的时候,jvm先去执行它的静态块和静态属性。所以这个时候执行

1
2
//子类静态属性
public static Singleton testOut = new Singleton(1);

在这里我们注意,执行这个语句的时候,它会调用子类的构造方法,而这个构造方法会先执行父类的构造方法,在执行下面的语句,所以这里的输出是

1
2
Test 1 Construct!-Father
Test 1 Construct!

然后按顺序执行下面的静态块

1
2
3
4
//子类静态块
static {
System.out.println("Test Static");
}

这个时候输出的是:

1
Test Static

这个时候,在main方法之前,已经对类完全初始化了。于是jvm开始执行main方法。
在main方法的第一句,我们new了一个子类

1
Singleton t = new Singleton(2);

按照上面对子类构造函数的执行,不难理解这里会输出

1
2
Test 2 Construct!-Father
Test 2 Construct!

然后执行到下一句

1
Singleton.getInstance();

在这里,调用了子类的一个静态方法getInstance

1
2
3
4
//子类静态方法
public static Singleton getInstance(){
return Inner.testInstance;
}

这里,它调用了静态内部类的一个方法testInstance。这时候,我们需要注意,jvm这个时候并没有加载这个内部类。所以jvm会先去加载这个子类内部类Inner并初始化它。这个时候,我们已经对jvm初始化类的顺序有一定的了解了。它会按顺序去执行里面的静态块和静态属性

1
2
3
4
5
6
7
//子类静态内部类
private static class Inner{
public final static Singleton testInstance = new Singleton(3);
static {
System.out.println("TestInner Static!");
}
}

不难理解,这里也会调用父类的构造方法,所以输出是

1
2
3
Test 3 Construct!-Father
Test 3 Construct!
TestInner Static!


总结

到这里整个输出的过程我们分析就结束了。下面我们来总结下整个初始化的顺序:

  1. 先父后子。初始化时,jvm会先去初始化它的父类,再初始化它本身。而且只有在父类完全被初始化后,才会执行子类的初始化。
  2. 先静态后非静态,最后构造函数。jvm会根据顺序先执行类里面的静态属性和静态块,然后再按顺序执行非静态块和非静态属性,最后才初始化构造函数(其实构造函数这里并没有执行)。而且需要注意的是,静态的只是在加载字节码(.class文件)时执行一次,不管new多少次。而非静态的每new一次就会执行一次。
  3. 类只有在调用的时候才会被加载,进而初始化。
  4. 类只有在jvm加载字节码并初始化后才能被构造成实例对象。

写在最后

后来,OSC中有个朋友说jdk厂家不同,加载顺序会有差异,比如IBM的jdk,这里就不探讨了。如果以后遇到了,会同步更新在这里。关于jvm如何加载字节码的,可以看看这篇文章Java杂谈3——类加载机制与初始化顺序.

研究JDK和JVM还有好长的路要走!


参考文档