Java中的类加载器

631 阅读11分钟

本文翻译自Class Loaders in Java如有侵权请告知

  1. Class Loader介绍 Class Loader负责在JVM运行时动态加载Java Classs.同时他也是JRE(Java Runtime Environment)的一部分.因此.JVM在运行java程序时不需要知道关于底层文件或者文件系统如何读取,这都得感谢Class Loader.

    同时这些Java classes不会一次性全部载入内存,而是在应用程序需要的时候按需加载.这就是Class Loader的勇武之地,他们负责加载classes到内存.

    在这篇教程中,我们将讨论不同类型的内置(JRE自带)class loader,他们如何工作和如何实现我们自己的class loader.

  2. 内置Class Loader的类型 通过下面简单的例子,我们将学习如何使用不同种类的Class Loader加载不同的classes.

    public void printClassLoaders() throws ClassNotFoundException {
    
        System.out.println("Classloader of this class:"
            + PrintClassLoader.class.getClassLoader());
    
        System.out.println("Classloader of Logging:"
            + Logging.class.getClassLoader());
    
        System.out.println("Classloader of ArrayList:"
            + ArrayList.class.getClassLoader());
    }
    

    当我们执行上面的方法可以得到如下输出:

    Class loader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
    Class loader of Logging:sun.misc.Launcher$ExtClassLoader@3caeaf62
    Class loader of ArrayList:null
    

    我们可以看到,这里有三种不同的class loader,application extension 和 bootstrap(显示为null).

    Application loader加载示例方法所在的类,Application class Loader(应用类加载器) 又叫 System Class Loader(系统类加载器)从classpath加载我们的classes文件.

    接下来我们会看到Extension Loader加载Logging 类,Extension class loaders(扩展类加载器) 加载的类都是Java核心标准的扩展.

    最终,Bootstrap Loader加载ArrayList类,Bootstrap Class Loader(启动类加载器)又叫Primordial Class Loader(原始类加载器)是其他加载器的父类.

    然而,我们可以看到最后一行的输出,对于ArrayList的输出显示为null,这是因为Bootstrap Class Loader(启动类加载器)使用Native Code(本机代码)写的,而非Java.所以它不能显示为Java类.由于这个原因,Bootstrap Class Loader(启动类加载器)在JVMs的行为会有所不同.

    现在我们来讨论三个类加载器的更多细节.

     1. Bootstrap Class Loader(启动类加载器)
    
         Java类被java.lang.ClassLoader的实例加载,然而class loader本事也是一个类,因此,谁来加载 java.lang.ClassLoader便成了一个问题.
    
         这就是Bootstrap Class Loader(启动类加载器)又叫Primordial Class Loader(原始类加载器)的勇武之地.
    
         它主要负责加载JDK内部类,典型的rt.jar和其他在$JAVA_HOME/jre/lib目录下的核心库.另外,Bootstrap Class Loader(启动类加载器)还会充当其他类的父级.
    
         这个Bootstrap Class Loader(启动类加载器)是JVM的一部分,它使用Native Code编写,正如上面的例子所指出的一样.不同平台对于这个特殊的类加载器可能会有不同的实现.
    
     2. Extension class loaders(扩展类加载器)
    
         Extension class loaders(扩展类加载器)Bootstrap Class Loader(启动类加载器)的子集,负责加载Java标准核心的扩展类,这样这些扩展类就可以被平台上的所有应用程序使用.
    
         Extension class loaders(扩展类加载器)从JDK的扩展目录加载,通常是$JAVA_HOME/lib/ext目录或其他系统属性java.ext.dirs中的目录.
    
     3. System Class Loader(系统类加载器)
    
         Application class Loader(应用类加载器) 又叫 System Class Loader(系统类加载器),从另一方面来说,负责加载应用程序级别的类到JVM,它从classpath环境变量中的目录加载文件,可以使用-classpath 或 -cp命令选项改变classpath路径,同时他还是Extension class loaders(扩展类加载器)的子级.
    
  3. Class Loader如何工作的

    Class Loader是JRE(Java Runtime Environment)[Java运行时]的一部分.当JVM请求类的时候,Class Loader尝试定位类的位置,并且类的完全限定的类名加载这些类的定义到运行时

    Java.lang.ClassLoader.loadClass()就是负责加载类的定义到运行时.它尝试加载类基于完全限定名称.

    如果这个类没有被加载,它将尝试使用父类加载器来加载.这个过程会被递归

    最后,如果他的父类加载器没有找到这个类,子类加载器会调用java.net.URLClassLoader.findClass()方法在文件系统中查找.

    如果最终还是没能找到这个类,便会抛出java.lang.NoClassDefFoundError or java.lang.ClassNotFoundException.异常.

    我们可以看一下ClassNotFoundException的例子:

    java.lang.ClassNotFoundException: com.baeldung.classloader.SampleClassLoader    
        at java.net.URLClassLoader.findClass(URLClassLoader.java:381)    
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)    
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)    
        at java.lang.Class.forName0(Native Method)    
        at java.lang.Class.forName(Class.java:348)
    

    如果我们通过调用java.lang.Class.forName()来完成事件序列,我们可以理解它首先尝试通过父类加载器加载类,然后java.net.URLClassLoader.findClass()来查找Class本身。

    如果它们最后还是没有找到类,会抛出ClassNotFoundException异常.

    这三点是Class Loader非常重要的特性.

    1. 双亲委派模型

      Class Loader遵循双亲委派模型,在请求查找类或资源时,ClassLoader实例将类或资源的搜索委托给父类加载器。

      假设我们有一个加载Application class到JVM的请求,System Class Loader(系统类加载器)会委托Extension class loaders(扩展类加载器)加载,后者又委托给Bootstrap Class Loader(启动类加载器)加载.当Bootstrap Class Loader(启动类加载器)和Extension class loaders(扩展类加载器)加载不成功时.System Class Loader(系统类加载器)才会尝试自己加载.

    2. 唯一性

      作为使用双亲委派模型的结果,他很容易保证classes的唯一性,因为他总是向上委托父级加载classes.

      如果当前父级Class Loader无法找到此类时,当前类加载器的实例才会尝试自己加载

    3. 可见性

      此外,子加载器对父加载器加载的类是可见的.

      列如,System Class Loader(系统类加载器)加载的类可以看到Extension class loaders(扩展类加载器)和Bootstrap Class Loader(启动类加载器)加载的类.反过来则不行.

      为了说明这一点,如果Class A被Application class Loader(应用类加载器)加载同时Class B被Extension class loaders(扩展类加载器)加载,A和B类对其他Application class Loader(应用类加载器)加载的类而言是看见的.

      尽管如此,Class B只对于Extension class loaders(扩展类加载器)加载的其他类是可见的.

  4. 自定义Class Loader

    在使用文件系统的情况下,默认内建的Class Loader能够满足大多数情况.

    然而,有时候存在需要我们从硬盘驱动器或者网络中加载类的情况.我们需要利用自定义Class Loader来完成.

    在这一章节,我们将介绍自定义类加载器的一些其他用例,我们将演示如何创建一个。

    1. 自定义类加载器用例

      自定义类加载不仅仅是在运行时加载类,一些用例包括: 1. 动态修改存在的字节码,列如:weaving agents 2. 动态创建符合用户需求的类,列如:JDBC,通过动态类加载完成不同驱动程序实现之间的切换。 3. 在为具有相同名称和包的类加载不同的字节码时实现类版本控制机制.此功能同样可以通过URL Class Loader(加载jar通过URL)实现或自定义Class Loader

      还有更多类加载器能够派上用场的例子.

      列如,浏览器使用自定义类加载器从网站上加载可执行内容,浏览器可以使用独立的Class Loader从不同的网页加载applets.applet viewer会包含一个Class Loader用于加载远程服务器上的网站内容.而不是本地文件系统.

      并且通过HTTP加载raw bytecode文件,以及把他们转换成JVM中的类.即使这些applet具有相同的名称,如果由不同的类加载器加载,它们被视为不同的组件.

      现在我们已经了解到了Class Loader的应用,我们来实现一个ClassLoader的子类用于扩展和总结JVM如何加载类的功能.

    2. 创造一个自定义类加载器

      为了便于说明,假设我们需要从FTP中加载类,内建Class Loader无法完成这样的加载,因为他不存在于classpath路径中.

      public class CustomClassLoader extends ClassLoader {
          public CustomClassLoader(ClassLoader parent) {
              super(parent);
          }
          public Class getClass(String name) throws ClassNotFoundException {
              byte[] b = loadClassFromFTP(name);
              return defineClass(name, b, 0, b.length);
          }
      
          @Override
          public Class loadClass(String name) throws ClassNotFoundException {
      
              if (name.startsWith("com.baeldung")) {
                  System.out.println("Loading Class from Custom Class Loader");
                  return getClass(name);
              }
              return super.loadClass(name);
          }
      
          private byte[] loadClassFromFTP(String fileName)  {
              // Returns a byte array from specified file.
          }
      }
      

      通过上述的例子,我们扩展了默认类加载器并且从 com.baeldung 包中加载类.

      我们在构造函数中传入了父类 ,然后我们使用FTP类加载器传入一个完全限定的类名作为输入.

  5. 理解java.lang.ClassLoader

    接下来我们讨论java.lang.ClassLoader一些基本方法来获得ClassLoader如何运行的更清晰的概念.

    1. loadClass()方法

      public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
      

      这个方法负责通过给定完全限定类名来加载类.

      Java虚拟机调用loadClass方法并设置resolve来解析类的引用,然而,并不是任何时候都需要解析一个类,如果我们只需要确定类存不存在,则设置resolve为false.

      这个方法是Class Loader的入口.

      我们可以通过阅读源码来尝试理解loadClass()的内部工作原理.

      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.
                      c = findClass(name);
                  }
              }
              if (resolve) {
                  resolveClass(c);
              }
              return c;
          }
      }
      

      该方法的默认实现搜索类通过以下顺序: 1. 调用findLoadedClass(String)方法查看类是否已经加载 2. 调用父类的loadClass(String)加载类 3. 如果没有找到调用findClass(String)加载类

    2. defineClass()方法

      protected final Class<?> defineClass(
          String name, byte[] b, int off, int len) throws ClassFormatError
      

      这个方法在我们使用类之前转换Byte数组为类的实例,我们需要解析它.

      如果这个Byte数组不是一个有效的类,它将会抛出一个ClassFormatError错误.

      同样,我们也无法重写这个类,因为它也被标记为final

    3. findClass()方法

      protected Class<?> findClass(
              String name) throws ClassNotFoundException
      

      这个方法使用完全限定名称作为参数, 我们在自定义的Class Loader中重写这个方法,并且遵循双亲委派原则来加载classes.

      同样,如果父Class Loader无法加载classes,loadClass()则调用这个方法.

      默认实现,如果父Class Loader无法加载classes则抛出ClassNotFoundException异常.

    4. getParent()方法

      public final ClassLoader getParent()
      

      这个方法返回父类的Class Loader以便委派.

      有些实现,就像第二节中看到的第一个例子,使用null代表Bootstrap Class Loader加载器.

    5. getResource()方法

    public URL getResource(String name)
    

    这个方法试图查找给定名字的资源.

    它首先会委托父类加载器解析资源,如果父加载器为null,则搜索内置到虚拟机中的类加载器的路径。

    如果此方法失败了就会调用findResource(String)来查找资源,资源名称可被指定为classpath的相对路径或绝对路径.

    他返回一个用于读取资源的URL对象,如果无法找到资源或者调用者没有足够的权限返回资源,则返回null.

    值得注意的一点是java从classpath中加载资源.

    最后,Java中的资源加载被认为是与位置无关的,因为只要环境设置为查找资源,代码运行的位置无关紧要。

  6. Context Classloaders

    一般来说,contest class loaders为J2SE中提供另一种加载委派方法.

    就像我们之前学习的,JVM中的Class Loader是阶级式的模型,每一个Class Loader都拥有一个父级Class Loader,除了Bootstrap Class Loader.

    然而,有时候JVM需要动态加载应用开发者提供的类或者资源 时,我们需要考虑这个问题.

    举个栗子,在JNDI中,核心功能由rt.jar中的引导类实现。但是这些JNDI类可能会加载由独立供应商实现的JNDI提供程序(部署在应用程序的classpath中).此方案要求引导类加载器(父类加载器)加载应用程序加载器(子类加载器)可见的类.

    J2SE的委托在这种情况下无法工作,要解决这个问题,我们需要寻找另一种类加载方法,并且他可以使用线程上下文加载器(thread context loaders)来实现.

    java.lang.Thread有一个getContextClassLoader()方法,它返回一个ContextClassLoader针对特定的线程.在加载类和其他资源时ContextClassLoader由它的线程创建者提供.

    如果这个值没有设置.那么他默认使用父线程的类加载器.

  7. 结论

    类加载器对于执行Java程序至关重要,作为本文的一部分,我们提供了一个很好的介绍.

    我们讨论了不同类型的类加载器加载机制,Bootstrap, Extensions 和 System class loaders.Bootstrap 类加载器作为所有类加载器的父级,负责加载JDK内部类,Extensions 和 system,另一方面,它们分别从java extensions 目录和classpath中加载类.

    然后我们讨论了关于类加载器如何工作并且讨论了一些关于类加载器的特性例如双亲委派 可见性 和 唯一性,接着我们简要阐述了如何创建一个自定义类加载器,最后我们介绍了Context Loader.

    一如既往的,我们提供了代码样本,见:Github