java 类加载器详解 -- 双亲委派模式及实现

2018-05-16 20:29:38   最后更新: 2018-05-21 19:01:53   访问数量:359




上一篇日志中,我们简要介绍了 java 类的加载、初始化流程和三种类加载器

java 类加载过程及类加载器

本篇日志中,我们就来介绍一下 java 类加载器的工作原理

 

java 类加载器基于三个原则:委托、可见性、唯一性

 

委托原则 -- 双亲委派模式

委托原则把加载类的请求转发给福类加载器,委托父类加载器进行类的加载,只有当父类加载器无法找到或不能加载类时,才由子加载类进行加载

假设有一个名为 Abc.class 的应用程序特定的类文件

加载这个类文件的第一个请求发送到 Application 类加载器

Application 类加载器会委托它的父级 Extension 类加载器,而 Extension 类加载器委托给 Bootstrap 类加载器,Bootstrap 类加载器在 rt.jar 中寻找类文件,由于没有找到,请求转发到 Extension 类加载器,Extension类加载器在 jre/lib/ext 目录中寻找类文件并尝试加载类文件

如果找到了类文件,这时Extension类加载器将会加载类文件(Application类加载器将永远不会加载类文件),但是如果 Extension 类加载器没有加载类文件,那么 Application 类加载器将会从 java 类路径中加载类文件

 

 

 

  • 类 A 引用到类 B,则由类 A 的加载器去加载类 B,保证引用到的类被一同载入到系统

 

可见性原则

可见性原则允许子类加载器查看由父类加载器加载的所有的类,但是父类加载器不能查看由子类加载器加载的类

这意味着,假如Application类加载器加载了 Abc.class 文件,那么尝试使用Extension类加载器去加载 Abc.class 文件,则会抛出异常 java.lang.ClassNotFoundException

 

唯一性原则

唯一性原则只允许加载一次类文件,这基本上是通过委托原则来实现的并确保子类加载器不重新加载由父类加载器加载过的类

一旦一个类被父级类加载器加载,则该类就不能被一个子级类加载器加载了

 

双亲委派模式是 jdk1.2 引入的类加载机制,它构成了一种带有优先级的类加载层次,这个机制保证了上述提到的唯一性原则,避免了类的重复加载

当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次

同时,双亲委派模式也保证了安全性,例如,java 核心 api 中定义的类在这个原则下会保证被启动类加载器加载,这样就防止了核心 API 被篡改

如果我们在其他路径上自定义核心 api 中的类,由于父类加载器路径下没有这个类,就只能反向委托给子类加载器加载,最终会通过系统类加载器加载该类,而由于访问权限,强制加载核心 API 包中的类会抛出以下异常

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

 

下面是类加载器的继承层次:

 

 

ClassLoader

可以看到,顶层的类加载器是一个抽象类 ClassLoader,这个类是除启动类加载器外所有类加载器的父类

 

  • public Class<?> loadClass(String name)

loadClass 方法是加载指定包名.类名的二进制类型,jdk1.2之后不再建议用户重写该方法,但用户可以直接调用该方法,如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass("className")

这个方法的实现就是调用了 ClassLoader 类中的 protected Class<?> loadClass(String name, boolean resolve) 方法

而在这个 protected 的 loadClass 方法中,实现了双亲委派机制:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 先在缓存中查找 class 对象,如果找到就不再重新加载 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); 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) { // 如果在父类加载器中没找到,则通过自定义实现的 findClass 方法加载 long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } // 判断是否需要解析 if (resolve) { resolveClass(c); } return c; } }

 

 

  • public final ClassLoader getParent()

看到 getParent 这个方法,很多人会直观的以为他是要获取父类的实例,事实上并不是这样的,他获取的是父加载器

public final ClassLoader getParent() { if (parent == null) return null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { // Check access to the parent class loader // If the caller's class loader is same as this class loader, // permission check is performed. checkClassLoaderPermission(parent, Reflection.getCallerClass()); } return parent; }

 

 

  • protected Class<?> findClass(String name)

通过 loadClass 方法的源码可以看到,双亲委派的实现较为简单,真正的加载过程就是调用 findClass 方法实现的

正如我们前面提到的,jdk1.2 以后,不建议用户去 override loadClass 方法,如果需要自定义类加载逻辑,那么就写在 findClass 方法中,这样就可以保证自定义类加载器也符合双亲委托模式

默认的实现很简单:

protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }

 

也就是说,如果继承 ClassLoader 的子类没有实现 findClass 方法去加载对应的类,就会抛出 ClassNotFoundException

 

  • protected final Class<?> defineClass(byte[] b, int off, int len)

defineClass 方法是用来将 byte 字节流解析成 jvm 能够识别的 class 对象的

通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象

defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象

例如,下面是一个 defineClass 方法的实现

@Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 获取类的字节数组 byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { //使用defineClass生成class对象 return defineClass(name, classData, 0, classData.length); } }

 

需要注意的是,如果直接调用defineClass()方法生成类的Class对象,这个类的Class对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行

 

  • protected final void resolveClass(Class<?> c)

这个方法完成 Class 对象的创建,同时解析该对象

这是一个在某些情况下确保类可用的必要方法,是否需要解析对象,依赖于 loadClass 方法的传入参数 resolve 是否为 true

 

  • public static ClassLoader getSystemClassLoader()

这个方法获取了系统默认的类加载器,他调用了 initSystemClassLoader 方法,initSystemClassLoader 方法中通过 Launcher 对象的 getClassLoader 方法获取到默认的类加载器

当我们需要创建自己的加载器时,就需要通过这个方法获取到父加载器,也就是会返回 AppClassLoader

 

SercureClassLoader

我们看到,所有的类加载器都继承了 SercureClassLoader

SercureClassLoader 类扩展了 ClassLoader,新增了几个与使用相关的代码源 (对代码源的位置及其证书的验证) 和权限定义类验证 (主要指对class源码的访问权限) 的方法,一般我们不会直接跟这个类打交道,更多是与它的子类 URLClassLoader 有所关联

 

URLClassLoader

ClassLoader 作为一个抽象类,很多方法比如 findClass、findResource 等等都没有实际的实现,而 URLClassLoader 这个实现类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助获取 Class 字节码流等功能

通常,如果没有过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免自己编写 findClass 方法及其获取字节码流的方式,从而让自定义的类加载器更加简洁

 

上面是 URLClassLoader 的类图,URLClassLoader 中最重要的就是成员变量 ucp,他是 URLClassPath 类型的,因此图中增加了 URLClassPath 类图

URLClassPath 类负责找到要加载的字节码,再读取成字节流,最后通过defineClass()方法创建类的Class对象

URLClassLoader 类和 URLClassPath 类的构造方法都有一个 URL[] 参数,该参数就是字节码文件的路径,也就是说在创建 URLClassLoader 对象时必须要指定这个类加载器的到那个目录下找 class 文件

 

  • protected Class<?> findClass(final String name)

在类图中我们可以看到,URLClassPath 有两个内部类,分别是 FileLoader 和 JarLoader,顾名思义,他们分别是用来处理 URL[] 路径中字节码是文件或是 jar 包的情况的,他们都继承了 Loader 类,jvm 就是通过调用 ClassLoader 的 findClass 方法操作 Loader 类实例将 class 字节码流加载到内存中的,最后利用内存中的字节码流创建 class 对象实例

protected Class<?> findClass(final String name) throws ClassNotFoundException { final Class<?> result; try { result = AccessController.doPrivileged( new PrivilegedExceptionAction<Class<?>>() { public Class<?> run() throws ClassNotFoundException { String path = name.replace('.', '/').concat(".class"); Resource res = ucp.getResource(path, false); if (res != null) { try { return defineClass(name, res); } catch (IOException e) { throw new ClassNotFoundException(name, e); } } else { return null; } } }, acc); } catch (java.security.PrivilegedActionException pae) { throw (ClassNotFoundException) pae.getException(); } if (result == null) { throw new ClassNotFoundException(name); } return result; }

 

这里调用了我们上面介绍过的 defineClass 方法,他就是用来将 byte 字节流解析成 jvm 能够识别的 class 对象的

 

ExtClassLoader 与 AppClassLoader

 

如上图所示,ExtClassLoader 与 AppClassLoader 都是 Launcher 类的内部类,同时他们都继承自 URLClassLoader

Launcher 类就是 jvm 加载类的入口,他创建了内部类 ExtClassLoader 与 AppClassLoader

 

  • public Class loadClass(String name, boolean resolve)

ExtClassLoader 并没有重写 loadClass 方法,仍然使用上面所说的 ClassLoader 的 loadClass 方法

APPClassLoader 重写了 loadClass:

public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException { int var3 = var1.lastIndexOf(46); if (var3 != -1) { SecurityManager var4 = System.getSecurityManager(); if (var4 != null) { var4.checkPackageAccess(var1.substring(0, var3)); } } if (this.ucp.knownToNotExist(var1)) { Class var5 = this.findLoadedClass(var1); if (var5 != null) { if (var2) { this.resolveClass(var5); } return var5; } else { throw new ClassNotFoundException(var1); } } else { return super.loadClass(var1, var2); } }

 

ascii 编码 46 是英文句号(.),程序入口处的判断就是如果传入了包名,则进行包权限的校验

同时,如果没有找到,则调用父类 URLClassLoader 的 loadClass 方法,从而同样保证了双亲委派原则

 

上图中我们看到,ExtClassLoader 与 AppClassLoader 都继承自 URLClassLoader 并且都是 Launcher 的内部类,我们要想知道 ExtClassLoader 与 AppClassLoader 的关系就要从 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); } Thread.currentThread().setContextClassLoader(this.loader); // 权限相关代码 }

 

可以看到,在 Launcher 的构造器中,首先创建了 ExtClassLoader 实例,然后将 ExtClassLoader 实例传递给 AppClassLoader 从而让 ExtClassLoader 成为 AppClassLoader 的父加载器

在上述代码中,我们看到,他设置 AppClassLoader 实例为 ContextClassLoader -- 线程上下文加载器,那么,什么是线程上下文加载器呢?敬请关注我们的下一篇博客

 

Java类加载器工作原理 https://blog.csdn.net/archleaner/article/details/42913498

深入理解Java类加载器(ClassLoader) -- https://blog.csdn.net/javazejian/article/details/73413292

类加载器的工作原理 -- http://www.importnew.com/6581.html

类加载器详解 -- https://www.cnblogs.com/dongguacai/p/5879931.html

一看你就懂,超详细java中的ClassLoader详解 -- https://blog.csdn.net/briblue/article/details/54973413

 

 






技术帖      技术分享      java      classloader      双亲委派      findclass      java类加载机制     


京ICP备15018585号