Java 字节码分析成员变量初始化顺序

一、无继承关系

class Demo {
    private int p1 = 100; 	// 实例变量显式初始化
    private static int p2 = 10; 	// 静态变量显式初始化

    {
        p1 = 101; // 构造代码块初始化
    }

    static {
        p2 = 11; // 静态代码块初始化
    }

    public Parent() {  // 构造函数初始化
        this.p1 = 102;
        this.p2 = 12;
    }

}

1、静态变量

上述代码中静态变量 p2 赋值流程:

(1)p2 = 0 :类加载的准备阶段,静态变量设置默认零值。
(2)p2 = 10 —> p2 = 11:类加载的初始化阶段,执行 clinit()。

clinit() 由编译器按语句在源文件中出现的顺序自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的

(3)p2 = 12 :类对象执行 init() 函数,包括显式初始化,构造代码块和构造函数。

总结静态变量赋值顺序

类加载的准备阶段静态变量初始零值 —> 类加载的初始化阶段静态变量显式赋值 —> 类加载的初始化阶段静态代码块赋值 —> 构造函数赋值

2、实例变量

上述代码中实例变量 p2 赋值流程:

(1)p1 = 0:类对象在堆空间分配,实例字段设置默认零值
(2)p1 = 100 —> p1 = 101 —> p1 = 102:类对象执行 init 函数,包括显式初始化,构造代码块和构造函数

总结实例变量赋值顺序

实例变量默认零值 —> 显式初始化 —> 构造代码块赋值 —> 构造函数赋值

3、整体流程

上述代码中实例变量 p1 与静态变量 p2 整体赋值流程:

(1) p2 = 0 :父类加载的准备阶段,静态变量设置默认零值
(2)p2 = 10 —> p2 = 11:父类加载的初始化阶段,执行 clinit()
(3)p1 = 0:子类对象在堆空间分配,实例字段设置默认零值
(4)p1 = 100 —> p1 = 101 —> p1 = 102,p2 = 12 :类对象执行 init() 函数,包括显式初始化,构造代码块和构造函数

总结变量赋值顺序

类加载的准备阶段静态变量初始零值 —> 类加载的初始化阶段静态变量显式赋值 —> 类加载的初始化阶段静态代码块赋值 —> 实例变量默认零值 —> 实例变量显式初始化 —> 实例变量构造代码块赋值-> 构造函数给静态变量、实例变量赋值

二、继承中的变量赋值

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();
    }
}

上述代码变量赋值流程:

(1)p2 = 0 :父类加载的准备阶段,静态变量设置默认零值
(2)p2 = 10 , p2 = 11:父类加载的初始化阶段,执行clinit()
(3)s2 = 0 :子类加载的准备阶段,静态变量设置默认零值
(4)s2 = 10 , s2 = 11:子类加载的初始化阶段,执行clinit()
(5)p1 = 0,s1 = 0 :子类对象在堆空间分配,实例字段设置默认零值,这里实例字段也包括从父类中继承来的字段
(6)p1 = 100, p1 = 101, p1 = 102,p2 = 12 :父类对象执行 init() 函数,包括显式初始化,构造代码块和构造函数
(7)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 
19 putfield #2 <Son.s1>
22 aload_0 
23 pop 
24 bipush 12 
26 putstatic #3 <Son.s2> 
29 return 

Son.init() 方法中可以看到,它会首先调用父类的 init() 方法,然后对子类实例变量进行显示赋值,接着执行构造代码块中的赋值动作,最后进行构造函数中的赋值动作,所以这里 s1 的变化就是:刚开始为为默认零值,然后显式赋值为 101,接接构造代码块将 s1 赋值为 101,最后在构造函数中将 s1 赋值为 102,s2 赋值为 12。

总结变量赋值顺序

父类静态变量初始零值 -> 父类静态变量显式赋值 -> 父类静态代码块赋值 -> 子类静态变量初始零值 -> 子类静态变量显式赋值 -> 子类静态代码块赋值 -> 实例变量默认零值 -> 父类构造代码块赋值 -> 父类构造函数赋值 -> 子类构造代码块赋值 -> 子类构造函数赋值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值