Java 从头再来(一):大话 Java 类加载过程以及类加载器的双亲委派机制

一:Java 的类加载过程

在说类加载器之前,先说一下 Java 的类加载过程,既然是做 Java 的牛逼程序员,总不能面试的时候就说直接 java -jar 运行 jar 包 或者 war 包不就完了,面试嘛 。。。咱们肯定要高大上一点,开始下面的装逼模板(别墨迹,直接背就行):

-> 执行 java 命令 
-> 调用脚本文件创建 Java 虚拟机(C++实现)
-> 创建引导类加载器(C++实现)
-> C++ 通过 JNI 的方式调用 Java 代码(sun.misc.Launcher.getLauncher() ),创建 JVM 启动实例(Launcher 的实例)
-> 获取自己的类加载器(AppClassLoader 的实例):launcher.getClassLoader() 
-> 调用 loadClass 加载要运行的类 :classLoader.loadClass("com.havemail.demo.XXX") 
-> C++ 发起调用,执行 main 方法 
-> Java 程序运行结束,JVM 销毁

loadClass 的类加载过程主要分为:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载。其中验证、准备与解析也可以称为连接过程。

  • 加载:加载字节码文件(class 文件),用到哪些类就加载哪些 class 文件;在加载过程中会在内存中生成每个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。
  • 验证:验证字节码文件的正确性,安全性等。
  • 准备:给类的静态变量分配内存,并赋予默认值,这些变量所使用的内存都将在方法区中进行分配 。
  • 解析:将常量池内的符号引用(比如 main 方法)替换为直接引用(内存的指针或句柄等)的过程,该过程为静态链接过程。
  • 初始化:执行静态代码块,对类的静态变量初始化为指定的值;将默认值换成声明类变量时指定的初始值。

二:Java 类载器

Java 主要提供了下面几种类加载器(3个默认1个自定义):

1. 引导类加载器(BootStrap ClassLoader)

  • 负责加载 %JAVA_HOME%/jre/lib 目录下的核心类库,比如 rt.jarcharsets.jar 等;
  • 负责加载 %JAVA_HOME%/jre/classes 中的类。
  • 由 JVM 底层的 C/C++ 实现,Java 代码访问不到该加载器。
  • 加载 -Xbootclasspath 参数指定的路径。

可以通过代码查看该类加载器加载了哪些 jar 包:

public class Main {
    public static void main(String[] args) {
        URL[] url = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < url.length; i++) {
            System.out.println(url[i].toExternalForm());
        }
    }
}

输出结果如下所示:

file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/classes

2. 扩展类加载器(Ext ClassLoader)

全称:Extensions ClassLoader

  • 加载 Java 的扩展列库,默认加载 %JAVA_HOME%/jre/lib/ext 扩展目录中的 jar 文件以及由 java.ext.dirs 系统变量指定的路径下的 jar 文件。
  • Ext ClassLoader 是 App ClassLoader 的父加载器(并不是说的父类)。

扩展类加载器加载的路径可以通过代码查看:

public class Main {
    public static void main(String[] args) {
        System.out.println(System.getProperty("java.ext.dirs"));
    }
}

输出如下:

/Users/roc/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java

3. 应用程序类加载器(App ClassLoader)

【注】因为通过 ClassLoader.getSystemClassLoader() 获得的是 App ClassLoader,也可称为 系统类加载器

  • 负责加载 ClassPath 路径下的类包与文件,主要是加载开发人员编写的类文件。
  • Java 程序默认的类加载器。
  • 加载 -classpath 指定的类包与文件。
  • 如果没有特别指定,则用户自定义的任何类加载器都会将该加载器作为它的父加载器。

通过代码可以获取 ClassPath 的加载路径:

public class Main {
    public static void main(String[] args) {
        System.out.println(System.getProperty("java.class.path"));
    }
}

输出的结果有点多,仅放关键的部分:

....
/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/lib/tools.jar:/Users/roc/Downloads/HelloJava/out/production/HelloJava:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar

【注】在 IDEA 中如果是纯 Java 项目则为 out 目录,J2EE 项目则为 target 目录。

4. 自定义类加载器(Custom ClassLoader)

  • 加载用户自定义路径下的类包。可以通过继承自 java.lang.ClassLoader 类的方式来实现,以满足特定的场景。

5. 类加载器初始化过程

先看源码再说:

public class Launcher {
   // ① 
    private static Launcher launcher = new Launcher();
    private ClassLoader loader;
    
    // ... 

    // ④
    public static Launcher getLauncher() {
        return launcher;
    }

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            // ②
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            // ③
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        // ...
    }

    public ClassLoader getClassLoader() {
        return this.loader;
    }
    
    // ...
}

具体过程如下:

① 当系统启动的时候会创建一个 Launcher 的静态实例(采用了单例模式,保证只有一个实例);

② Launcher 的构造方法会创建一个 ExtClassLoader 的实例,

③ 然后将 ExtClassLoader 的实例传入到 AppClassLoader 作为其父加载器并返回 AppClassLoader 实例,然后赋值给 loader

④ 然后 C++ 通过 JNI 调用 Launcher 类中的 getLauncher() 方法获取到实例。

【注】JVM 默认使用 getClassLoader() 方法返回的类加载器(AppCLassLoader 的实例)来加载我们的应用,所以 AppClassLoader 是 Java 的默认类加载器。

三:类加载器加载机制 – 双亲委派机制

上面说的仅仅是类加载器的初始化流程,接下来就是类加载器的加载机制(也就是当虚拟机需要加载某个类了,这个类如何加载到 JVM 中的过程),Java 的类加载机制使用的是 – 双亲委派机制,那么什么是双亲委派机制呢?

【注】

  • 类加载器加载类的时候,并不会将所有的类完全加载,在程序启动的时候仅仅加载用到的类。
  • 加载器加载返回的内容是:在内存中生成的这个类的 java.lang.Class 对象
- 我们自己写了一个 Demo 类,然后运行的时候用到了这个类。
- JVM 想要加载 Demo 类,先让应用程序类加载器加载,应用程序类加载器从已加载的类中查找,如果有则直接返回;没有的话,然后应用程序类会委托给扩展类加载器来加载(委托);
- 扩展类加载器收到后,从其已加载的类中查找,如果有则直接返回;没有的话,然后会委托给启动类加载器来加载(委托);
- 启动类加载器收到后,从其已加载的类中查找 Demo 类,找到了直接返回;找不到就从自己的加载路径中加载 Demo 类,找到了直接返回,找不到则将请求派给扩展类加载器(派遣);
- 扩展类加载器收到后,就从自己的加载路径中加载 Demo 类,找到了直接返回,找不到则将请求派给应用类加载器(派遣);
- 应用类加载器收到后,则从自己的加载路径中加载 Demo 类,找到了直接返回,如果找不到则会抛出异常:ClassNotFound

这里是拿App ClassLoader 、Ext ClassLoader 与 Bootstrap ClassLoader 举例,如果有自定义加载器的话也是一样的道理,默认 AppClassLoader 是 自定义加载器父加载器,他们四者的层级关系入下图所示:

【注】简单一句话总结:双亲委派机制就是先找父亲加载,不行再由儿子自己加载;引导类加载器没有上一级,只能是加载。

四:常见面试题

1. 为什么要使用双亲委派机制

  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要让子 ClassLoader 再次加载一边,保证类的唯一性。
  • 沙箱安全机制:防止开发人员恶意重写 JDK 原有的核心类,从而导致程序有很大的安全问题。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注