JAVA类加载器双亲委派与自定义扩展

最近思考写一个JAVA框架,一边学习Spring框架源码,一边整理JDK反射技术相关文档,今天学习到ClassLoader类加载器,就记录下自己的学习心得。

  • 目录结构
  1. 文档说明
  2. UML类图
  3. 成员方法
  4. Tomcat加载器
  5. 如何自定义类加载器

1 文档说明

类文档来自JDK源码的英文,翻译重要部分。

英文:A class loader is an object that is responsible for loading classes. The
class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a
"class file" of that name from a file system.
中文:类加载器是一个负责加载各种类的对象,ClassLoader类是一个抽象类。给定一个类的二进制名称,类加载器应尽力去定位或生成构建该类定义的数据。一个典型的策略是把类名称转化成一个文件名,然后从文件系统读取类文件。

英文:Applications implement subclasses of ClassLoader in order to extend the manner in which the Java virtual machine dynamically loads classes.
中文:应用程序实现ClassLoader的子类,扩展JVM动态加载类的方式。

英文:The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine's built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance.
中文:ClassLoader类使用代理模型去搜索类和资源,每个ClassLoader实例都有一个关联的双亲类加载器。当请求查找一个类或资源时,在自己尝试发现类或资源前,ClassLoader实例将查询请求代理给双亲类加载器。虚拟机内置的类加载器,叫做"bootstrap加载器",没有双亲加载器而可以作为其它ClassLoader的双亲类加载器。

英文:Class loaders that support concurrent loading of classes are known as parallel capable class loaders and are required to register themselves at their class initialization time by invoking the ClassLoader.registerAsParallelCapable method. Note that the ClassLoader class is registered as parallel capable by default. However, its subclasses still need to register themselves if they are parallel capable.
中文:支持并发加载功能的加载器,被大家称为并行支持类加载器,要求在类加载器被初始化时,调用ClassLoader.registerAsParallelCapable方法向虚拟机注册。注意,ClassLoader类默认注册为并行支持。但是,它的子类如果只是并行加载,任然需要自己向虚拟机注册。

英文:In environments in which the delegation model is not strictly hierarchical, class loaders need to be parallel capable, otherwise class loading can lead to deadlocks because the loader lock is held for the duration of the class loading process.
中文:在不是严格的层级代理模型的环境中,类加载器需要支持并行加载,否则类加载过程会导致死锁,因为加载锁在类加载过程中被相互持有。

英文:Normally, the Java virtual machine loads classes from the local file system in a platform-dependent manner. For example, on UNIX systems, the virtual machine loads classes from the directory defined by the CLASSPATH environment variable.
中文:通常,JVM通过平台相关的文件系统加载类。例如,在UNIX系统,虚拟机从CLASSPATH环境变量指定的目录加载类。

英文:However, some classes may not originate from a file; they may originate from other sources, such as the network, or they could be constructed by an application. The method defineClass(String, byte[], int, int) converts an array of bytes into an instance of class. Instances of this newly defined class can be created using Class.newInstance
中文:但是,一些类不是来源于文件,她们可能创建于其他来源,例如网络,或者被应用程序构造出来。方法defineClass(String, byte[], int, int)可以转换一个字节数组成一个类实例。这个新定义的类实例可以用Class.newInstance方法创建。

英文:The methods and constructors of objects created by a class loader may
reference other classes. To determine the class(es) referred to, the Java virtual machine invokes the loadClass method of the class loader that originally created the class.
中文:被类加载器创建的对象构造函数或成员方法可能引用其他类,为了确定这些被引用的目标类,JVM调用创建该类对象的加载器loadClass方法。

英文:For example, an application could create a network class loader to download class files from a server.
中文:举例来说,一个应用程序可以创造一个网络类加载器从服务器下载类文件。

示例代码

ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();

英文:The network class loader subclass must define the methods findClass and loadClassData to load a class from the network. Once it has downloaded the bytes that make up the class, it should use the method defineClass to create a class instance.
中文:网络类加载器子类必须定义findClassloadClassData方法从网络上加载一个类,一旦从网络上下载组成类数据字节后,它应当使用defineClass方法去创建一个类实例。

示例代码

class NetworkClassLoader extends ClassLoader {
          String host;
          int port;
 
          public Class findClass(String name) {
              byte[] b = loadClassData(name);
              return defineClass(name, b, 0, b.length);
          }
 
          private byte[] loadClassData(String name) {
              // load the class data from the connection
              ...
          }
}

2 UML类图

2.1 ClassLoader声明

public abstract class ClassLoader
抽象类,必须被子类化,实现抽象方法后才能实例化;
系统默认使用sun.misc.Launcher的内部类ExtClassLoader和AppClassLoader,两个类都是ClassLoader的后代。

2.2 UML类图

梳理ClassLoader子类实现,以及系统默认使用的AppClassLoader,整理UML类图如下:


ClassLoader类图.png

注意:系统默认创建sun.misc.Launcher,Launcher默认构造函数根据系统配置,先加载ExtClassLoader,然后再加载AppClassLoader,ExtClassLoader类实例作为AppClassLoader的双亲类加载器。

ClassLoader依赖UML类图

ClassLoader类依赖图2.png

2.3 类加载顺序

类加载器ClassLoader的loadClass方法,实现类加载功能。
加载步骤

  1. 同步锁。调用getClassLoadingLock(String name)方法,获取加载同步锁,默认支持并行加载,用类名称name映射锁对象,实现加载不同类的并行支持;
  2. 缓存查找。调用findLoadedClass(String name)方法,在已加载的类缓存中查找,成功则跳转步骤6;
  3. 双亲委派。如果双亲加载器parent不为空,调用parent.loadClass加载类对象,成功泽跳转步骤6;
  4. VM加载。调用findBootstrapClassOrNull方法,委托虚拟机Bootstrap类加载器装载,成功则跳转步骤6;
  5. 本类加载器this接手,调用findClass(String name)方法定位并加载类对象;
  6. 链接类对象。调用resolveClass方法,虚拟机完成类链接。

3 成员方法

3.1 构造函数

  • 方法声明
public class ClassLoader{
    protected ClassLoader(); //编号1
    protected ClassLoader(ClassLoader parent); //编号2
    private ClassLoader(Void unused, ClassLoader parent); //编号3
}
  • 调用顺序
    三个构造函数外部调用者都不能直接访问,调用顺序:编号1->编号2->编号3。
  1. 默认构造函数调用getSystemClassLoader方法,传递双亲类加载器给编号2的构造函数;
  2. 编号2构造函数检查传入的双亲类加载器parent安全访问权限,调用编号3构造函数;
  3. 编号3构造函数初始化内部控制成员变量。

3.2 成员方法

3.2.1 loadClass方法

  • 方法作用
    loadClass方法,根据类名称装载类对象,是ClassLoader的核心方法,类加载器的双亲委派加载机制就是在这个函数实现的,另外,根据类全限定名称获取同步锁支持并行加载。
  • 方法声明
public Class loadClass(String name) throws ClassNotFoundException;
protected Class loadClass(String name, boolean resolve)  throws ClassNotFoundException;

注意:loadClass(String name)函数由JVM调用,它直接调用loadClass(name, false)由第二个函数实现加载逻辑。

  • 源码分析
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) {
                        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) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }

步骤分析

  1. synchronized(getClassLoadingLock(name))实现类加载同步,支持两种同步机制。支持并行加载的子类,每个类全限定名称对应一个锁;不支持并行的子类,在this对象上加锁;
  2. Class c = findLoadedClass(name);,调用findLoadedClass方法查找已加载的缓存类对象;
  3. 如果有双亲类加载器,c = parent.loadClass(name, false);,请求委托给双亲类加载器,实现双亲委托类加载机制;
  4. 如果没有双亲类加载器,c = findBootstrapClassOrNull(name);,请求虚拟机bootstrap加载器加载类对象;
  5. 如果双亲与VM的类加载器没有成功,c = findClass(name);,调用this的findClass方法,实现自定义类加载逻辑;
  6. 若需要链接resolve==true,执行resolveClass(c)实现类连接。注意:resolve入参为false,也就是说,类链接动作应该在函数返回后,在VM加载器中完成。

3.2.2 getClassLoadingLock方法

  • 方法作用
    获取加载器同步锁,根据是否存在双亲加载器,支持并行加载或串行加载两种同步机制。
  • 源码分析
    // Maps class name to the corresponding lock object when the current
    // class loader is parallel capable.
    // Note: VM also uses this field to decide if the current class loader
    // is parallel capable and the appropriate lock object for class loading.
    private final ConcurrentHashMap parallelLockMap;
    
    protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
    }

执行逻辑

  1. 检查并行加载是否支持,if(parallelLockMap != null)
  2. 若不支持,加载this对象;
  3. 若支持,执行Object newLock = new Object();创建新锁对象,执行lock = parallelLockMap.putIfAbsent(className, newLock);,查看名称对应锁对象是否存在并设置,如果存在,用旧锁,不存在则用新锁;

注意:使用ConcurrentHashMap对象,是支持类加载器并行加载的关键,同时,根据类全限定名称,采用多个同步锁机制,解决类加载的性能瓶颈。

3.2.3 findLoadedClass方法

  • 方法作用
    从JVM已加载类对象缓存查找类名称,查看是否已加载,如果加载则返回类对象,否则返回null。
  • 方法源码
    /**
     * Returns the class with the given binary name if this
     * loader has been recorded by the Java virtual machine as an initiating
     * loader of a class with that binary name.  Otherwise
     * null is returned.
    **/
    protected final Class findLoadedClass(String name) {
        if (!checkName(name))
            return null;
        return findLoadedClass0(name);
    }

    private native final Class findLoadedClass0(String name);

逻辑简单,检查名称是否符合规范后,直接调用本地方法findLoadedClass0

3.2.4 findBootstrapClassOrNull方法

  • 方法作用
    调用VM类加载器BootstrapClassLoader,成功返回类对象,否则返回null。
  • 方法源码
    /**
     * Returns a class loaded by the bootstrap class loader;
     * or return null if not found.
     */
    private Class findBootstrapClassOrNull(String name)
    {
        if (!checkName(name)) return null;

        return findBootstrapClass(name);
    }

    // return null if not found
    private native Class findBootstrapClass(String name);

方法的逻辑很简单,检查名称是否符合规范,调用本地方法findBootstrapClass

3.2.5 findClass方法

  • 方法作用
    根据类名称定位与装载类对象。
  • 方法源码
    protected Class findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

函数直接抛出异常ClassNotFoundExeception,说明必须在子类重载findClass函数,实现类定位、数据加载与类定义逻辑。

3.2.6 defineClass方法

  • 方法作用
    将组成类字节码的二进制数据转换为类对象,核心函数,有几个变体。
  • 方法声明
protected final Class defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError; //编号0
protected final Class defineClass(String name, byte[] b, int off, int len,
       ProtectionDomain protectionDomain) throws ClassFormatError; //编号1
protected final Class defineClass(String name, java.nio.ByteBuffer b,
                                         ProtectionDomain protectionDomain); //编号2

private native Class defineClass0(String name, byte[] b, int off, int len,
                                         ProtectionDomain pd);
private native Class defineClass1(String name, byte[] b, int off, int len,
                                         ProtectionDomain pd, String source);
private native Class defineClass2(String name, java.nio.ByteBuffer b,
                                         int off, int len, ProtectionDomain pd,
                                         String source);

从方法声明上可以看出,实现一个简单的自定义类加载器,只需要根据类名称获取到类对象字节码的二进制数据即可,不管数据是存储在数据库,还是来自于网络服务。

  • 方法源码
    protected final Class defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError //编号0
    {
        return defineClass(name, b, off, len, null);
    }

    protected final Class defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError //编号1
    {
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class c = defineClass1(name, b, off, len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }

    protected final Class defineClass(String name, java.nio.ByteBuffer b,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError //编号2
    {
        int len = b.remaining();

        // Use byte[] if not a direct ByteBufer:
        if (!b.isDirect()) {
            if (b.hasArray()) {
                return defineClass(name, b.array(),
                                   b.position() + b.arrayOffset(), len,
                                   protectionDomain);
            } else {
                // no array, or read-only array
                byte[] tb = new byte[len];
                b.get(tb);  // get bytes out of byte buffer.
                return defineClass(name, tb, 0, len, protectionDomain);
            }
        }

        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class c = defineClass2(name, b, b.position(), len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }

从源码看出:编号0的defineClass直接调用编号1,编号2的逻辑基本一致。编号1执行逻辑如下:

  1. 保护域。调用preDefineClass获取类对象的保护域;
  2. 类源码。执行defineClassSourceLocation(proectionDomain)获取类对象源码,不一定能获取源码;
  3. 生成类。执行本地方法Class c = defineClass2(name, b, b.position(), len, protectionDomain, source);生成名称与二进制字节码对应类对象;
  4. 证书签名。执行postDefineClass(c, protectionDomain);,处理类签名与证书比对;
  5. 返回类对象。

3.2.7 resolveClass方法

  • 方法作用
    链接类对象。
  • 方法源码
    /**
     * Links the specified class.  This (misleadingly named) method may be
     * used by a class loader to link a class.  If the class c has
     * already been linked, then this method simply returns. Otherwise, the
     * class is linked as described in the "Execution" chapter of
     * The Java™ Language Specification.
    **/
    protected final void resolveClass(Class c) {
        resolveClass0(c);
    }

    private native void resolveClass0(Class c);

文档注释:链接指定类。命名有一定误导性,这个方法可能被类加载器用来链接一个类。如果类c已经被链接,直接返回c,如果没有被链接过,VM执行链接过程。


4 Tomcat加载器

根据《深入理解JAVA虚拟机》一书介绍,主流的Java Web服务器,如Tomcat、Jetty、WebLogic等,都实现了自定义的类加载器,而且一般还不止一个。

4.1 面临问题

作者分享,Web服务器写自己类加载器的原因在于要解决以下问题:

  1. 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。两个不同应用程序可以会依赖同一个第三方类库的不同版本,服务器应当能够保证两个独立应用程序的类库可以互相独立使用;
  2. 部署在同一个服务器上的两个Web应用程序使用的Java类库可以互相共享,避免大量浪费内存等资源;
  3. 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。一般来说,基于安全考虑,服务器所使用的类库应该与应用程序的类库相互独立。
  4. 支持JSP应用的Web服务器,需要支持HotSwap功能。

上述问题,决定部署Web应用时,单独的一个ClassPath不能满足需求,所以各大Web服务器都采用多个不用含义的ClassPath路径供用户存放第三方类库。被纺织到不同路径中的类库,具有不同的访问范围和服务对象,通常每一个目录都会有一个响应的自定义类加载器去加载放置在里面的Java类库。

4.2 ClassPath

Tomcat服务器通常有四个放置类库的ClassPath,Tomcat的目录结构可以设置3组(/common/、/server/与/shared/),单默认不一定开放,可能只有/lib/目录存在,另外还有应用程序自身的/WEB-INF/*目录。


目录含义

  • /common/*目录。类库可被Tomcat和所有的Web应用程序共同使用。
  • /server/*目录。类库可以被Tomcat使用,对所有的Web应用程序不可见。
  • /shared/*目录。类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
  • /WebApp/WEB-INF/*目录。类库仅被Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

4.3 类加载器

对应几个类库目录,Tomcat自定义对应的类加载器对类库进行加载和隔离,这些加载器按照经典的双亲委派模型来实现。关系图如下:


Tomcat类加载模型.png

关系图解读

  1. 蓝色的三个是JVM自己实现的类加载器,后面5个浅色的都是Tomcat自定义实现的类加载器。
  2. Tomcat通过5个不同层级的类加载器,实现服务器与应用程序隔离、应用程序与应用程序隔离。
  3. Catalina ClassLoader实现Tomcat服务器自己的类加载器。
  4. WebApp ClassLoader的实例有0个或多个,与部署Web应用程序成正比,实现应用程序与应用程序相隔离。
  5. WebApp中有多个JSP文件,每一个JSP文件对应一个JasperLoader类加载器。

根据作者介绍,Tomcat6以后,为了简化用户部署与配置,Tomcat整合/common、/server、与/shared三个目录为/lib一个目录,但是,可以通过配置开启者几个目录。作者分享的Tomcat类加载器模型对接下来我要实现的框架十分有帮助。


5 如何自定义类加载器

5.1 源码文档案例

根据ClassLoader源码文档翻译,以及ClassLoader成员方法介绍,实现一个简单的类加载器只需要继承ClassLoader并重载findClass方法即可。在文档翻译部分有一个示范源码框架:

class NetworkClassLoader extends ClassLoader {
          String host;
          int port;
 
          public Class findClass(String name) {
              byte[] b = loadClassData(name);
              return defineClass(name, b, 0, b.length);
          }
 
          private byte[] loadClassData(String name) {
              // load the class data from the connection
              ...
          }
}

本案例中,NetworkClassLoader从ClassLoader继承,重载findClass方法,调用loadClassData方法获取类对应字节码数据后,调用defineClass方法生成类对象。至于loadClassData怎么实现,从数据库、调用Web服务,怎么干就随意了。


5.2 JDK类加载器

阅读ClassLoader.initSystemClassLoader函数源码,发现系统默认使用的ExtClassLoader扩展类加载器与AppClassLoader应用程序类加载器,是sun.misc.Launcher的内部类,两者都从URLClassLoader继承。借用上面的类关系图:


ClassLoader类图.png

有心要学习JDK类加载机制的朋友,不妨从这两个类的源码阅读与跟踪调试入手。

5.3 实现建议

要实现一个自定义的类加载器,从学习sun.misc.Launcher.AppClassLoader开始,继承URLClassLoader比较顺手,祝大家都顺利搞出自己的类加载器。

你可能感兴趣的:(JAVA类加载器双亲委派与自定义扩展)