Java 类的加载机制

/ Java技术 / 没有评论 / 3881浏览

Java 类的加载机制

类的生命周期

类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。

2020318111629-class-load

其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的“开始”(仅仅指的是开始,而非执行或者结束,因为这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用或者激活另一个阶段)。

其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。

何时开始类的初始化

什么情况下需要开始类加载过程的第一个阶段:加载。虚拟机规范中并没强行约束,这点可以交给虚拟机的的具体实现自由把握,但是对于初始化阶段虚拟机规范是严格规定了如下几种情况,如果类未初始化会对类进行初始化。

  1. 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;
  2. 调用一个类的静态方法的时候。
  3. 使用java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
  4. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  5. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
  6. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic,REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

以上 6 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:

加载

加载是类加载的一个阶段,注意不要混淆。

加载过程完成以下三件事:

其中二进制字节流可以从以下方式中获取:

验证

确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备

类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值(默认值),使用的是方法区的内存。

实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。(实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次)

初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。

public static int value = 123;

如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。

public static final int value = 123;

解析

将常量池的符号引用替换为直接引用的过程。

初始化

初始化阶段才真正开始执行类中的定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 () 方法的过程。

在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

() 方法具有以下特点:

public class Test {
    static {
        i = 0;                // 给变量赋值可以正常编译通过(赋值)
        System.out.print(i);  // 这句编译器会提示“非法向前引用”(访问)
    }
    static int i = 1;
}

类加载器

实现类的加载动作。在 Java 虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。

类与类加载器

两个类相等:类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。

这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。

类加载器分类

Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:

Java 开发人员的角度看,类加载器可以划分得更细致一些:

双亲委派模型

应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。

下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。

2020318151114-parent-delegate-model
工作过程

一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载。

好处

使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而是的基础类得到统一。

例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个java.lang.Object 的类并放到 ClassPath 中,程序可以编译通过。因为双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。正因为rt.jar 中的 Object 优先级更高,因为程序中所有的 Object 都是这个 Object

实现

以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;
 
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
 
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
 
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

自定义类加载器实现

FileSystemClassLoader 是自定义类加载器,继承自java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。

java.lang.ClassLoader 类的方法loadClass() 实现了双亲委派模型的逻辑,因此自定义类加载器一般不去重写它,而是通过重写 findClass() 方法。

public class FileSystemClassLoader extends ClassLoader {
 
    private String rootDir;
 
    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }
 
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }
 
    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
 
    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
}

例题(判断输出)

public class B {
    //静态变量
    static int i = 1;
    //静态语句块
    static {
        System.out.println("Class B1:static blocks,i=" + i);
    }
    //非静态变量
    int j = 1;

    //静态语句块
    static {
        i++;
        System.out.println("Class B2:static blocks,i=" + i);
    }
    //构造函数
    public B() {
        i++;
        j++;
        System.out.println("constructor B: " + "i=" + i + ",j=" + j);
    }
    //非静态语句块
    {
        i++;
        j++;
        System.out.println("Class B:common blocks," + "i=" + i + ",j=" + j);
    }
    //非静态方法
    public void bDisplay() {
        i++;
        System.out.println("Class B:static void bDisplay(): " + "i=" + i + ",j=" + j);
        return;
    }
    //静态方法
    public static void bTest() {
        i++;
        System.out.println("Class B:static void bTest():    " + "i=" + i);
        return;
    }
}

public class A extends B {
    //静态变量
    static int i = 1;

    //静态语句块
    static {
        System.out.println("Class A1:static blocks" + i);
    }

    //非静态变量
    int j = 1;

    //静态语句块
    static {
        i++;
        System.out.println("Class A2:static blocks" + i);
    }

    //构造函数
    public A() {
        super();
        i++;
        j++;
        System.out.println("constructor A: " + "i=" + i + ",j=" + j);
    }

    //非静态语句块
    {
        i++;
        j++;
        System.out.println("Class A:common blocks" + "i=" + i + ",j=" + j);
    }

    //非静态方法
    public void aDisplay() {
        i++;
        System.out.println("Class A:static void aDisplay(): " + "i=" + i + ",j=" + j);
        return;
    }

    //静态方法
    public static void aTest() {
        i++;
        System.out.println("Class A:static void aTest():    " + "i=" + i);
        return;
    }

    public static void main(String[] args) {
        A a = new A();
        a.aDisplay();
    }
}

以上两个类,B是A的父类,输出结果如下:

1.Class B1:static blocks,i=1
2.Class B2:static blocks,i=2
3.Class A1:static blocks1
4.Class A2:static blocks2
5.Class B:common blocks,i=3,j=2
6.constructor B: i=4,j=3
7.Class A:common blocksi=3,j=2
8.constructor A: i=4,j=3
9.Class A:static void aDisplay(): i=5,j=3

分析

  1. 首先按定义顺序加载父类的静态变量和静态代码块,所以输出了1、2两行
  2. 然后按定义顺序加载子类的静态变量和静态代码块,所以输出了3、4两行
  3. 然后加载父类的非静态代码块,输出了第5行
  4. 然后加载父类的构造函数,输出第6行
  5. 接着加载子类的非静态代码块,输出了第7行
  6. 然后加载子类的构造函数,输出第8行
  7. 最后执行子类的非静态方法

总结顺序如下

  1. 父类静态变量或静态代码块(按先后定义顺序)
  2. 子类静态变量或静态代码块(按先后定义顺序)
  3. 父类非静态代码块
  4. 父类构造函数
  5. 子类非静态代码块
  6. 子类的构造函数