类的加载

53 阅读3分钟

类的加载

  1. 加载类: 当首次访问一个类的静态成员时,Java虚拟机会加载该类。这个过程包括找到对应的.class文件,并执行如下的一些底层的初始化工作。

  2. 初始化静态成员:

    • 静态变量和静态初始化块会按照它们在代码中出现的顺序进行初始化。
    • 静态方法不需要初始化,它们在类加载后就可用。
  3. 实例化过程:

    • 分配内存:为新对象分配内存。
    • 初始化实例变量:按照代码中的顺序和对应构造函数中的实现来初始化实例变量。
    • 构造函数执行:当所有实例变量都已经初始化后,构造函数会被执行。

    请注意,静态成员(包括静态初始化块和静态变量)只会在类首次加载时初始化一次,而实例成员会在每次创建类的新实例时被初始化。

如果考虑到继承,这个过程可能会更加复杂,因为父类的静态成员和实例成员初始化会在子类的相应成员之前完成初始化。

底层的初始化工作

  1. 加载 (Loading) :

    • 查找:JVM 首先尝试找到指定的 .class 文件。这可以在文件系统中、JAR 文件中,甚至是通过网络等方式进行。
    • 读入:一旦找到 .class 文件,JVM 会读入文件并将其内容加载到内存中。这个过程由类加载器 (class loader) 完成。
    • 字节码验证:为了确保 .class 文件没有被篡改并且不会对 JVM 造成伤害,JVM 会进行字节码验证。它会检查二进制代码是否有任何违背 Java 语言规范的部分,例如检查类型安全性、确保没有不允许的数据类型转换等。
  2. 链接 (Linking) :

    • 验证 (Verification) :除了上述的基本字节码验证之外,还有更深入的验证过程,确保代码满足其他语言的规范。
    • 准备 (Preparation) :JVM 为类变量(也称为静态变量)分配内存,并设置它们的默认初始值。例如,int 类型的静态变量将被设置为 0,对象引用将被设置为 null 等。
    • 解析 (Resolution) :这是一个可选的步骤,涉及将符号引用替换为直接引用。符号引用是一个逻辑引用,直接引用是一个指向数据或方法的指针或偏移量。
  3. 初始化 (Initialization) :

    • 在这个步骤中,JVM 执行类的静态初始化块和静态变量初始化。这些代码是按照它们在源代码中出现的顺序执行的。

此外,当 JVM 加载一个类或接口时,它还保持着对其他类或接口的引用,这些类或接口可能在运行时被需要。这通常涉及递归地加载其他的类,进而涉及上述的所有步骤。

示例

来举个例子

public class Main {
    private static final Main instance = new Main();

    private static boolean a = initA();

    private boolean b = a;

    private static boolean c = a;

    private static boolean initA() {
        return true;
    }

    private static boolean getC() {
        return c;
    }

    public static void main(String[] args) {
        System.out.println(instance.b + " " + getC());
    }
}

在这个特定例子里:

  1. 加载类开始,类中所有的静态方法都将可用。
  2. 静态变量instance首先被初始化,这会触发实例化过程
  3. 这又会导致实例变量b被初始化。由于此时静态变量a还没有被初始化(因为我们还在初始化instance),因此b将被初始化为false(布尔类型的默认值)。
  4. 到此为止,实例化过程结束。
  5. 然后静态方法initA()被调用,静态变量a被设置为true
  6. 静态变量c然后被初始化,它的值也被设置为true,因为此时a已经是true

最后,在main函数中:

  • instance.b的值是false,因为当instance被初始化时,a还没有被初始化。
  • getC()返回c的值,即true

因此,程序的输出将是:

false true

如果交换一二行位置,则结果均为true