#|什么是双亲委派(Tomcat是如何扩展ClassLoader的?)

ClassLoader作用 在JVM中,加载某个类时,通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,并在内存中创建对应的Class对象作为方法区这个类的各种数据的访问入口。虚拟机设计团队通过类加载器ClassLoader来完成整个过程。通常,二进制字节流可以是jar包或者war包里的.class文件,也可以是来自远程服务器提供的字节流。流程如图:
#|什么是双亲委派(Tomcat是如何扩展ClassLoader的?)
文章图片

ClassLoader加载时机 JVM中类的加载属于延迟加载,并不是一次性就把所需要的类全部加载到内存中。通常,如下场景会触发类加载过程:

  • 当通过new 关键字创建实例对象时,当前类或者其父类在初始化前会被加载。
  • 静态类的属性进行读写操作时,或者调用静态方法时,当前类或者其父类在初始化前会被加载。
  • 直接调用Class.forName方法直接获取对应类的Class对象操作。
ClassLoader内置实现类 在JVM中,系统内置了多个ClassLoader实现类,用于加载不同路径的字节码文件。重要的实现类有Bootstrap ClassLoader,URLClassLoader,Extension ClassLoader和 Application ClassLoader,详细介绍分别如下:
  • 启动类加载器( Bootstrap ClassLoader),该类由C++实现,在JVM启动时,会把位于JAVA_HOME\lib目录中的Java核心类库, 或者被-Xbootclasspath参数所指定的路径中的类库,加载到虚拟机内存中,并且只能是虚拟机识别的文件名称(例如,rt.jar),即使把其他未识别名称的jar文件放到lib文件夹下也不会被加载。
  • UrlClassPath类加载器(URLClassLoader),根据UrlClassPath指定的路径加载类,Extension ClassLoader和 Application ClassLoader都是继承自URLClassLoader;Extension ClassLoader定义了从JAVA_HOME\lib\ext系统路径加载,Application ClassLoader定义了从用户类路径加载。
  • 扩展类加载器(Extension ClassLoader),该类由Launcher的内部类ExtClassLoader实现。它负责加载JAVA_HOME\lib\ext目录中的, 或者被java.ext.dirs系统变量所指定的路径中的所有类库。
  • 应用程序类加载器( Application ClassLoader) ,该类由Launcher的内部类AppClassLoader实现,它负责加载用户类路径( ClassPath) 上所指定的类库, 开发者可以直接使用这个类加载器。 如果应用程序中没有自定义过自己的类加载器, 一般情况下这个就是程序中默认的类加载器。
ClassLoader双亲委派加载模式 在JVM中ClassLoader类通过loadClass方法和findClass方法通过对应类型名称加载二进制字节流,通过defineClass方法把二进制字节流转换为Class对象,ClassLoader部分源码如下:
// ClassLoader.class /** * 默认的加载二进制字节流 */ public Class loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }/** * 为了兼容双亲委派模式的加载,扩展的二进制字节流加载方法 */ protected Class findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }/** * 定义Class对象 */ protected final Class defineClass(String name, byte[] b, int off, int len) throws ClassFormatError { return defineClass(name, b, off, len, null); }

为了防止用户复写一些与Java内置的基础类名称相同的类(列如,例如,常见的Object类,String类)被加载到内存对Java内置的对象产生影响,JVM采用双亲委派模式对类进行加载。 在ClassLoader中设置父类ClassLoader 对象parent,在对类进行加载的时候,会首先调用当前ClassLoader父类的loadClass方法进行加载,如果父类ClassLoader加载失败,则再调用当前类的findClass进行加载。在父类ClassLoader的loadClass方法调用过程属于递归调用过程,最终,是首先使用最顶级的父类ClassLoader进行加载。调用过程的源码如下:
//ClassLoader.class /** * 加载类 */ 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) { long t0 = System.nanoTime(); try {if (parent != null) { // 递归地调用parent父类的loadClass获取Class对象 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 } // 如果父类地递归调用没有加载到,则调用当前类的findClass获取Class对象 if (c == null) { c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } }

在JVM中,采用默认的类加载器Application ClassLoader在用户类路径下加载java.lang.Object类的过程为例。加载流程如下图:
#|什么是双亲委派(Tomcat是如何扩展ClassLoader的?)
文章图片

当Application ClassLoader执行加载时,首先会调用parent父类Extension ClassLoader的loadClass方法,然后,继续调用其父类Bootstrap ClassLoader的loadClass方法加载。由于Object是java的核心类,因此在Bootstrap ClassLoader会成功加载到内存中。因此,在自定义ClassLoader时,通过实现findClass自定义加载二进制字节流的逻辑,避免破坏JVM的双亲委派模式。
ClassLoader典型扩展 在tomcat7中,使用WebappClassLoader,加载web服务中目录WEB-INF下的类,集成URLClassLoader类,采用双亲委派的模式。WebappClassLoader父类WebappClassLoaderBase中自定义findClass方法,实现WEB-INF下的二进制文件加载逻辑,部分源码如下:
// WebappClassLoaderBase.class @Override public Class findClass(String name) throws ClassNotFoundException {Class clazz = null; try {if (hasExternalRepositories && searchExternalFirst) { try { // 父类调用findClass clazz = super.findClass(name); }catch (RuntimeException e) {throw e; } } if ((clazz == null)) { try { // 自定义findClassInternal实现二进制文件加载 clazz = findClassInternal(name); } catch (RuntimeException e) {throw e; } } } catch (ClassNotFoundException e) { throw e; } return (clazz); }

【#|什么是双亲委派(Tomcat是如何扩展ClassLoader的?)】

    推荐阅读