Java成员变量初始化顺序完整版(附字节码分析)
先说结论:静态变量初始零值 -> 静态变量显式赋值 -> 静态代码块赋值 -> 实例变量默认零值 -> 构造代码块赋值 -> 构造函数赋值
如果考虑到父类,其初始化顺序为:父类静态变量初始零值 -> 父类静态变量显式赋值 -> 父类静态代码块赋值 -> 子类静态变量初始零值 -> 子类静态变量显式赋值 -> 子类静态代码块赋值 -> 实例变量默认零值 -> 父类构造代码块赋值 -> 父类构造函数赋值 -> 子类构造代码块赋值 -> 子类构造函数赋值
例子
class Parent {
private int p1 = 100; //实例变量显式初始化
private static int p2 = 10; //静态变量显式初始化
p1 = 101; //构造代码块初始化
static {
p2 = 11; //静态代码块初始化
public Parent() { //构造函数初始化
this.p1 = 102;
this.p2 = 12;
public class Son extends Parent{
private int s1 = 100;
private static int s2 = 10;
s1 = 101;
static {
s2 = 11;
public Son() {
this.s1 = 102;
this.s2 = 12;
public static void main(String[] args) {
Son son = new Son();
}
上述代码赋值流程:
- p2 = 0 (父类加载的准备阶段,静态变量设置默认零值)
- p2 = 10 , p2 = 11(父类加载的初始化阶段,执行clinit)
- s2 = 0 (子类加载的准备阶段,静态变量设置默认零值)
- s2 = 10 , s2 = 11(子类加载的初始化阶段,执行clinit)
- p1 = 0,s1 = 0 (子类对象在堆空间分配,实例字段设置默认零值,这里实例字段也包括从父类中继承来的字段)
- p1 = 100, p1 = 101, p1 = 102,p2 = 12 (父类对象执行init函数,包括显式初始化,构造代码块和构造函数)
- s1 = 100, s1 = 101, s1 = 102,s2 = 12 (子类对象执行init函数,包括显式初始化,构造代码块和构造函数)
字节码分析
从字节码角度来分析,对class文件进行反编译后,Son son = new Son()会转化为如下三条字节码指令:
0 new #4 <Son>
3 dup
4 invokespecial #5 <Son.<init>>
其中new指令会执行如下操作:首先判断该对象的类是否加载,如果没有加载,就进行加载。类加载的过程可以细分为加载、验证、准备和初始化5个阶段。其中 准备阶段会给静态变量分配默认零值 ,所以静态变量就算没有显式也是可以使用的。 在初始化阶段,会执行clinit方法 ,这个方法没有在java代码中定义,但是在java代码编译时,jvm会给每个类自动生成一个clinit方法,该方法会自动收集类中所有静态变量的赋值动作和静态代码块中的语句并执行,下面就是son类中clinit方法的字节码指令:
0 bipush 10
2 putstatic #3 <Son.s2>
5 bipush 11
7 putstatic #3 <Son.s2>
10 return
我们可以看到clinit方法中,会先将10压入操作数栈,然后赋值给静态变量s2,接着将11压入操作数栈,并赋值给静态变量s2,也就是clinit方法先执行显式赋值动作,然后执行static代码块中的动作。
还有一个重要的点就是在类加载的过程中,如果该类存在父类,就会先加载父类,然后加载子类,因此会先初始化父类静态变量,然后初始化子类静态变量。
完成类加载后,new指令就会在堆中分配一块内存空间给实例对象,虚拟机会将实例字段都初始化为零值,这一步操作保证了对象的实例字段在java代码中可以不赋初值就可以直接访问,程序能访问到这些字段的数据类型所对应的零值。这里的实例字段也包括从父类中继承下来的字段。
完成new指令后,进行dup指令,就是复制一份新建对象的引用到操作数栈中。然后执行invokespecial,该指令会调用Son.init方法,我们现在来看Son.init的字节码:
0 aload_0
1 invokespecial #1 <Parent.<init>>
4 aload_0
5 bipush 100
7 putfield #2 <Son.s1>
10 aload_0
11 bipush 101
13 putfield #2 <Son.s1>
16 aload_0
17 bipush 102