Flink 源码(九):阅读 Flink 源码前必会的知识(四)SPI 和 ClassLoader(一)ClassLoader

来源:https://mp.weixin.qq.com/s/PtmlneRo6AG4Fyb8y-Bvrw

一 简介

二、ClassLoader 类加载器

1、Java 中的类加载器以及双亲委派机制

Java 中的类加载器,是 Java 运行时环境的一部分,负责动态加载 Java 类到 Java 虚拟机的内存中。

有了类加载器,Java 运行系统不需要知道文件与文件系统。

那么类加载器,什么类都加载吗?加载的规则是什么?

Java 中的类加载器有四种,分别是:

  • BootstrapClassLoader,顶级类加载器,加载JVM自身需要的类;

  • ExtClassLoader,他负责加载扩展类,如 jre/lib/ext 或 java.ext.dirs 目录下的类;

  • AppClassLoader,他负责加载应用类,所有 classpath 目录下的类都可以被这个类加载器加载;

  • 自定义类加载器,如果你要实现自己的类加载器,他的父类加载器都是AppClassLoader。

  类加载器采用了双亲委派模式,其工作原理是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。

  如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。

  如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

双亲委派模式的好处是什么?

  第一,Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层次关系可以避免类的重复加载,当父类加载器已经加载过一次时,没有必要子类再去加载一次。

  第二,考虑到安全因素,Java 核心 Api 类不会被随意替换,核心类永远是被上层的类加载器加载。如果我们自己定义了一个 java.lang.String 类,它会优先委派给 BootStrapClassLoader 去加载,加载完了就直接返回了。

  如果我们定义了一个 java.lang.ExtString,能被加载吗?答案也是不能的,因为 java.lang 包是有权限控制的,自定义了这个包,会报一个错如下:

java.lang.SecurityException: Prohibited package name: java.lang

2、双亲委派机制源码浅析

Java 程序的入口就是 sun.misc.Launcher 类,我们可以从这个类开始看起。

下面是这个类的一些重要的属性,写在注释里了。

public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    // static launchcher 实例
    private static Launcher launcher = new Launcher();
    // bootclassPath ,就是 BootStrapClassLoader 加载的系统资源
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    // 在 Launcher 构造方法中,会初始化 AppClassLoader,把它作为全局实例保存起来
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;
    ......
}

这个类加载的时候,就会初始化 Launcher 实例,我们看一下无参构造方法。

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

        try {
            // 获得 AppClassLoader,并赋值到全局属性中
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
  
        // 把 AppClassLoader 的实例赋值到当前上下文的 ClassLoader 中,和当前线程绑定
        Thread.currentThread().setContextClassLoader(this.loader);
       // ...... 省略无关代码

    }

可以看到,先获得一个 ExtClassLoader ,再把 ExtClassLoader 作为父类加载器,传给 AppClassLoader。最终会调用这个方法,把 ExtClassLoader 传给 parent 参数,作为父类加载器。

而在初始化 ExtClassLoader 的时候,没有传参:

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

而最终,给 ExtClassLoader 的 parent 传的参数是 null。可以先记住这个属性,下面在讲 ClassLoader 源码时会用到这个 parent 属性。

 然后 Launcher 源码里面还有四个系统属性,值得我们运行一下看看,如下图

从上面的运行结果中,我们也可以轻易看到不同的类加载器,是从不同的路径下加载不同的资源。而即便我们只是写一个 Hello World,类加载器也会在后面默默给我们加载这么多类。

 

看完了 Launcher 类的代码,我们再来看 java.lang.ClassLoader 的代码,真正的双亲委派机制的源码是在这个类的 loaderClass 方法中。

   protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查这个类是否已经被加载了,最终实现是一个 native 本地实现
            Class<?> c = findLoadedClass(name);
            // 如果还没有被加载,则开始架子啊
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 首先如果父加载器不为空,则使用父类加载器加载。Launcher 类里提到的 parent 就在这里使用的。
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 如果父加载器为空(比如 ExtClassLoader),就使用 BootStrapClassloader 来加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
    
                // 如果还没有找到,则使用 findClass 类来加载。也就是说如果我们自定义类加载器,就重写这个方法
                if (c == null) {
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

这段代码还是比较清晰的,加载类的时候,首先判断类是不是已经被加载过了,如果没有被加载过,则看自己的父类加载器是不是为空。如果不为空,则使用父类加载器加载;如果父类加载器为空,则使用 BootStrapClassLoader 加载。

最后,如果还是没有加载到,则使用 findClass 来加载类。

类加载器的基本原理就分析到这里,下面我们再来分析一个 Java 中有趣的概念,SPI。

原文地址:https://www.cnblogs.com/qiu-hua/p/14489239.html