解析 Java 类加载机制

先来一道面试题

class Grandpa {
    static {
        System.out.println("爷爷在静态代码块");
    }
}

class Father extends Grandpa {
    static {
        System.out.println("爸爸在静态代码块");
    }

    public static int factor = 25;

    public Father() {
        System.out.println("我是爸爸~");
    }
}

class Son extends Father {
    static {
        System.out.println("儿子在静态代码块");
    }

    public Son() {
        System.out.println("我是儿子~");
    }
}

public class InitializationDemo {
    public static void main(String[] args) {
        System.out.println("爸爸的岁数:" + Son.factor);
    }
}

输出结果是什么???
反复分析

爷爷的静态代码块
爸爸的静态代码块
爸爸的岁数:25

对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块)。因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化(如果类 Father 当前还没有初始化),而不会触发子类的初始化

对于一开始的题目,现在我们可以确认 Son.factor 只会触发父类 Father 的初始化,Father 又会触发父类 Grandpa 的初始化(如果类 Grandpa 当前还没有初始化),因此:
先触发类 Grandpa 初始化,输出 爷爷在静态代码块
再触发类 Father 初始化,输出 爸爸在静态代码块
最后输出 爸爸的岁数:25

补充:如果此时再次调用 System.out.println("爸爸的岁数:" + Son.factor);,此时输出结果:

爷爷的静态代码块
爸爸的静态代码块
爸爸的岁数:25
爸爸的岁数:25

类只会在没有被初始化时才进行初始化,即类的初始化只进行一次

一、类的生命周期

七个阶段:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

1.1 加载:

通过一个类的全限定名,来获取此类的二进制字节码
将字节码所代表的静态存储结果转化为方法区的运行时数据结构
在 Java 堆中生成一个代表这个类的 Class 对象,作为方法区这些数据的访问路口

1.2 验证:

JVM 规范校验:文件格式验证、元数据验证、字节码验证、符号引用验证

1.3 准备

正式为静态变量分配内存并设置初始值(各数据类型的零值),这些内存在方法区中分配;静态变量的字段属性为常量(被 final 修饰)时,则会初始化为指定的值,而不是零值,类的成员变量的分配内存在初始化阶段才开始

1.4 解析

将符号引用替换为直接引用(直接指向目标的指针、相对偏移量)

1.5 初始化

为类的静态变量赋指定的初始值,对类对象进行初始化

1.6 使用

使用类之前,对类进行实例化,new 或反射,只有实例化了,才能通过对象的引用来访问对象的实例

1.7 卸载

执行垃圾回收

二、类初始化和对象初始化

2.1 类初始化方法

编译器会按照其出现顺序,收集类变量的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行

2.2 对象初始化方法

编译器会按照其出现顺序,收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法。对象初始化方法一般在实例化类对象的时候执行

再来一题

class Son extends Father {
    static Son instance = new Son();
	
    static {
        System.out.println("static Son");
    }
	
    {
        System.out.println("unStatic Son");
    }
	
    public Son() {
        System.out.println("construct Son");
    }
	
    protected void method() {
        System.out.println("method Son");
    }
	
    @Override
    public String toString() {
        return "toString Son";
    }
}

class Father {
    static final String TAG = "Father";
	
    static {
        System.out.println("static Father");
    }
	
    {
        System.out.println("unStatic Father");
    }
	
    public Father() {
        System.out.println("construct Father");
        method();
    }
	
    protected void method() {
        System.out.println("method Father");
    }
	
    @Override
    public String toString() {
        return "toString Father";
    }	
}

public class Demo {
    // 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类
    public static void main(String[] args) {
        System.out.println(Son.instance);
    }
}

输出结果:

// Son 对象的初始化会先触发父类 Father 的类初始化
static Father
// 父类 Father 对象的初始化
unStatic Father
construct Father
method Son
// Son 对象的初始化
unStatic Son
construct Son
// Son 类的初始化
static Son
// main() 方法输出,调用 Son 的 toString() 方法
toString Son

调用 Son.instance,其实调用的是 static Son instance = new Son();,所以会对 Son 进行类初始化,又因为父类 Father 还没有进行类初始化,所以会首先触发父类 Father 的类初始化,然后回到子类 Son,又因为在声明静态变量 instance 的同时,进行了 new Son() 操作,所以会先触发父类 Father 的对象初始化,然后进行子类 Son 的对象初始化,由 Son 的类初始化的顺序(代码顺序:静态代码块在静态字段之后)可知,此时会调用子类 Son 的静态代码块,最后执行 main() 方法所在类的 main() 方法中的输出,调用 Son 的 toString() 方法

问题来了:为什么 Son 类的初始化输出语句在 Son 对象的初始化输出语句的后面输出
其实,Son 的类初始化顺序为:

// 先执行初始化
static Son instance = new Son();
// 后执行初始化
static {
    System.out.println("static Son");
}

静态代码块和静态变量的执行顺序是根据代码编写先后顺序来的,而静态变量是又进行了 Son 对象的初始化操作,所以又会触发父类 Father 的类初始化(如果当前 Father 没有初始化)和对象的初始化

如果把 Son 的类初始化顺序改为:

// 先执行初始化(先输出 static Son)
static {
    System.out.println("static Son");
}
// 后执行初始化(对 Son 进行对象的初始化)
static Son instance = new Son();

那么整个输出结果:

// Son 对象的初始化会先触发父类 Father 的类初始化
static Father
// Son 类的初始化(在 Son 对象的初始化前)
static Son
// 父类 Father 对象的初始化
unStatic Father
construct Father
method Son
// Son 对象的初始化(在 Son 类的初始化后)
unStatic Son
construct Son
// main() 方法输出,调用 Son 的 toString() 方法
toString Son

最后一题

public class Book {
    public static void main(String[] args) {
        staticFunction();
    }

    static Book book = new Book();

    static {
        System.out.println("书的静态代码块");
    }

    {
        System.out.println("书的普通代码块");
    }

    Book() {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }

    public static void staticFunction(){
        System.out.println("书的静态方法");
    }

    int price = 110;
    static int amount = 112;
}

输出结果:

// 对 main() 方法所在的 Book 类进行类初始化(按顺序执行),代码顺序:静态变量在静态代码块之前

// 步骤一:静态变量
// 步骤 1.1:普通代码块
书的普通代码块
// 步骤 1.2:构造方法(对 Book 类进行对象初始化)
书的构造方法
// 步骤 1.3:因为 price 是类的成员变量,在类初始化时被赋值,即当前值为 110,而 amount 是类静态变量,因为对 amount 的赋值语句在静态代码块之后执行,所以此时 amount 的值还是 0
price:110,amount:0

// 步骤二:静态代码块
// 步骤 2.1:静态代码块(对 Book 类进行对象初始化)
书的静态代码块
// main() 方法输出,调用 Book 的 staticFunction() 静态方法
书的静态方法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值