转载

Java类加载器的准备和初始化

好久没有更新博客了,昨天在浏览微信公众号的时候看到了一道面试题,给出一个包含 main 方法的类,一些输出语句,让读者判断这些语句的输出顺序,其实也就是考察读者的对于 Java 类的初始化的顺序,看到这道题目的时候,我也就按照自己的理解得出了一个顺序,可是和答案却大相径庭,原因就在于对于 Java 类加载器在加载类时候的两个阶段: 准备初始化 没有足够的理解。今天我们在本文中就来聊聊类加载的这两个阶段

那道题

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

    static StaticTest st = new StaticTest();

    static {
        System.out.println("1");
    }

    {
        System.out.println("2");
    }

    StaticTest() {
        System.out.println("3");
        System.out.println("a=" + a + ",b=" + b);
    }

    public static void staticFunction() {
        System.out.println("4");
    }

    int a = 110;
    static int b = 112;
}

如果你能很简单的得到这段代码的输出结果,也就无需继续看下去了,如果结果和你得出的结果有差异,那不妨看看类加载器是如何来对这些变量做处理的。

答案如下

2
3
a=110,b=0
1
4

看那道题目之前我们先看些简单的吧!

类加载

Java类加载器的准备和初始化

上图是类加载的过程,涉及到类变量和类实例变量的初始化和赋值都在 准备初始化 这两个阶段,因此我们只讨论这两部分。

准备

此时是为类变量(静态变量或者静态代码块)分配内存并进行初始化的阶段。这些变量都是分配在 方法区 的,而不是在堆中。并且这里的初始化指的是数据类型的 零值 ,对于基本数据类型就是0,对于引用类型则是 null 。这里我们举两个例子

public static int value = 123;
public static Person person = new Person("Jacob");

在准备阶段, value 会被初始化为0, person 会被初始化为 null ,将 value 设置为 123 ,是在类加载的初始化阶段中,在 <clinit>() 方法中。但是有一种情况除外,如果一个字段是常量,这个字段就会在准备阶段赋值,如下

public static final int x = 999; // x会在准备阶段被赋值为999

初始化

在准备阶段被初始化为零值的那些变量会在初始化阶段赋值为在代码中定义的值。初始化阶段其实就是执行 <clinit>() 方法。 <clinit>() 方法是做的就是类变量的赋值动作和静态语句块。 并且变量的赋值顺序就是在代码源文件中的出现顺序 ,静态语句块只能访问到定义在它之前的变量,定义在它之后的变量可以赋值,但是不能访问。

public static int value = 123;
static { // 静态语句块
  System.out.println("This is a static code block");
}
public class Test {
  static {
    i = 0;  // 赋值可以通过
    Ssytem.out.print(i); // 引用无法通过编译(提示“非法向前引用”)
  }
  static int i = 1;
}

顺便说说实例的初始化顺序

实例的初始化函数是 <init>() ,它的执行顺序是:

  1. 父类变量初始化 和 父类语句块 (顺序是源码顺序)
  2. 父类构造函数
  3. 子类变量初始化 和 子类语句块 (顺序是源码顺序)
  4. 子类构造函数

对应的类初始化函数 <client>() 的执行顺序是:

  1. 父类静态变量初始化 和 静态语句块 (顺序是源码顺序)
  2. 子类静态变量初始化 和 子类静态语句块(顺序是源码顺序)

综合上面两个函数可以得到一个包含所有步骤的顺序

  1. 父类静态变量初始化 和 静态语句块 (顺序是源码顺序)
  2. 子类静态变量初始化 和 子类静态语句块(顺序是源码顺序)
  3. 父类变量初始化 和 父类语句块 (顺序是源码顺序)
  4. 父类构造函数
  5. 子类变量初始化 和 子类语句块 (顺序是源码顺序)
  6. 子类构造函数

注意这里说的是开始顺序,并不是结束顺序,实例的初始化可以在类的初始化之前完成,也就是说, <clinit>() 可能发生在 <init>() 之后,文章开头的例子就是这样。

题目解析

在本文的例子中,当准备阶段完成后,类变量会被赋值为以下值:

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

    static StaticTest st = new StaticTest(); // 此时 st 为 null

    static {
        System.out.println("1"); // 还未执行,这个会在<clinit>()执行, 也就是类加载的初始化阶段
    }

    {
        System.out.println("2");// 还未执行,这个会在<init>()执行
    }

    StaticTest() { // 还未执行,这个会在<init>()执行
        System.out.println("3");
        System.out.println("a=" + a + ",b=" + b);
    }

    public static void staticFunction() {
        System.out.println("4"); // 还未执行,这个会在类加载完成后被调用的时候执行
    }

    int a = 110; // 还未执行,这个会在<init>()执行
    static int b = 112; // 此时 b 为 0
}

完成准备阶段后,来到了初始化阶段 <clinit>() 的前半段,会在 <clinit>() 结束前,执行 <init>()

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

    static StaticTest st = new StaticTest(); // 此时 需要为st赋值,因此会进入 StaticTest 的 <init>() 函数

    static {
        System.out.println("1"); // 还未执行,需要先执行StaticTest 的 <init>() 函数,由于上一条语句需要实例化 StaticTest
    }

    {
        System.out.println("2");// 执行作为第1行输出,因为已经进入了 <init>() 函数 ---------
    }

    StaticTest() { // 执行,晚于实例代码块和实例变量
        System.out.println("3"); //第2行输出 ---------
        // 此时由于构造函数晚于实例代码块和实例变量的赋值,
        // 因此 a=110, 由于<clinit>()没有完成,所以 b=0
        System.out.println("a=" + a + ",b=" + b); //第3行输出---------
    }

    public static void staticFunction() {
        System.out.println("4"); // 还未执行,这个会在类加载完成后被调用的时候执行
    }

    int a = 110; // 执行,被赋值为110
    static int b = 112; // 此时 b 为 0
}

然后是 <clinit>() 的后半段

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

    static StaticTest st = new StaticTest(); // 赋值完成

    static {
        System.out.println("1"); // 作为第4行输出---------
    }

    {
        System.out.println("2");// 已经执行完毕
    }

    StaticTest() { // 已经执行完毕
        System.out.println("3");
        System.out.println("a=" + a + ",b=" + b); 
    }

    public static void staticFunction() {
        System.out.println("4"); // 作为第5行输出---------
    }

    int a = 110; // 已经在<init>()执行完毕
    static int b = 112; // 执行赋值操作, b 为 112
}

至此,对于变量的初始化应该就在比较明确了。

总结

重点还是记住下面的顺序,同时对 类加载过程有一定的了解。

  1. 父类静态变量初始化 和 静态语句块 (顺序是源码顺序)
  2. 子类静态变量初始化 和 子类静态语句块(顺序是源码顺序)
  3. 父类变量初始化 和 父类语句块 (顺序是源码顺序)
  4. 父类构造函数
  5. 子类变量初始化 和 子类语句块 (顺序是源码顺序)
  6. 子类构造函数
原文  https://jacobchang.cn/preparation-and-init-of-classloader.html
正文到此结束
Loading...