来源: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。