Java类的加载、链接、初始化

2022年8月18日10:16:42

Java 语言的类型可以分为两大类:基本类型(primitive types)和引用类型(reference types),基本类型是由Java 虚拟机预先定义好的。引用类型,Java 将其细分为四种:类、接口、数组类和泛型参数。由于泛型参数会在编译过程中被擦除(我会在专栏的第二部分详细介绍),因此 Java 虚拟机实际上只有前三种。在类、接口和数组类中,数组类是由 Java 虚拟机直接生成的,其他两种则有对应的字节流(如class文件)。

加载

Class 只有在必须要使用的时候才会被装载,Java 虚拟机不会无条件地装载 Class 类型。Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。这里指的 “使用”, 是指主动使用,主动使用只有下列几种情况:

  • 当创建一个类的实例时,比如使用 new 关键字,或者通过反射、克隆、反序列化。
  • 当调用类的静态方法时,即当使用了字节码 invokestatic 指令。
  • 当使用类或接口的静态字段时 (final 常量除外), 比如,使用 getstatic 或者 putstatic 指令。
  • 当使用 java.lang.reflect 包中的方法反射类的方法时。
  • 当初始化子类时,要求先初始化父类。
  • 作为启动虚拟机,含有 main 方法的那个类。

加载类处于类装载的第一个阶段。在加载类时,Java 虚拟机必须完成以下工作:

  • 通过类的全名,获取类的二进制数据流。
  • 解析类的二进制数据流为方法区内的数据结构。
  • 创建 java.lang.Class 类的实例,表示该类型。

启动类加载器负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)。除了启动类加载器之外,另外两个重要的类加载器是扩展类加载器(extension class loader)和应用类加载器(application class loader),均由 Java 核心类库提供。

扩展类加载器的父类加载器是启动类加载器。它负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)。

应用类加载器的父类加载器则是扩展类加载器。它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。

         加载采用双亲委派模型,每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。

链接

链接指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。

  • 验证:确保被加载类能够满足 Java 虚拟机的约束条件。一般Java 编译器生成的类文件必然满足 Java 虚拟机的约束条件。字节码注入的例子之后会介绍。
  • 准备:则是为被加载类的静态字段分配内存。构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表。
  • 解析:解析阶段的工作就是将类、接口、字段和方法的符号引用转为直接引用。符号引用就是一些字面量的引用,和虚拟机的内部数据结构和内存布局无关。在 class 文件被加载至 Java 虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用。在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。例如,对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。解析阶段的目的,正是将这些符号引用解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)

初始化

要初始化一个静态字段,可以在声明时直接赋值,也可以在静态代码块中对其赋值。如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成常量值(ConstantValue),其初始化直接由 Java 虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit >。

初始化,便是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。初始化触发的条件有:

1. 当虚拟机启动时,初始化用户指定的主类(main);

2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;

3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;

4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;

public class Singleton {

    private Singleton() {}

    private static class LazyHolder {

        static final Singleton INSTANCE = new Singleton();

    }

    public static Singleton getInstance() {

        return LazyHolder.INSTANCE;
    
        }

}

只有当调用Singleton.getInstance 时,程序才会访问 LazyHolder.INSTANCE,才会触发对LazyHolder 的初始化

5. 子类的初始化会触发父类的初始化;

6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;

7. 使用反射 API 对某个类进行反射调用时,初始化这个类;

8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

例子:

public class Singleton {

    private Singleton() {}

    private static class LazyHolder {
    
    static final Singleton INSTANCE = new Singleton();

    static {

        System.out.println("LazyHolder.<clinit>");

        }

    }

public static Object getInstance(boolean flag) {

    if (flag) return new LazyHolder[2];

    return LazyHolder.INSTANCE;

}

public static void main(String[] args) {

    getInstance(true);

    System.out.println("----");

    getInstance(false);

}

}

新建数组new LazyHolder[2]会加载元素类LazyHolder;不会初始化元素类。虚拟机必须知道(加载)有这个类,才能创建这个类的数组(容器),但是这个类并没有被使用到(没有达到初始化的条件),所以不会初始化,也不会链接元素类LazyHolder;

调用getInstance(false)的时候约等于告诉虚拟机,我要使用这个类了,你把这个类造好(链接),然后把static修饰的字符赋予变量(初始化)。

  • 作者:其实系一个须刨
  • 原文链接:https://blog.csdn.net/lianggx3/article/details/117113990
    更新时间:2022年8月18日10:16:42 ,共 3005 字。