跳至主要內容

类加载器那些事儿(一)

postjava大约 24 分钟

类加载器那些事儿(一)

在之前的文章《Java类的生命周期》open in new window我们谈了一下类的生命周期。 在这篇文章中,我们谈谈java的类加载器哪些事情。从下面的JVM架构图可以看到

JVM架构图
JVM架构图

class Loader subSystem负责管理和维护java类的生命周期的前三个阶段:

  • 加载
  • 链接
  • 初始化

当我们编写一个java的源文件后,我们对这个xxx.java编译会得到xxx.class的字节码文件,因为jvm只能运行字节码文件。为了能够使用这个class字节码文件,我们就会用到java中的ClassLoader。 而我们这篇文章就来说说java类加载器的那些事情。

ClassLoader是什么

ClassLoader顾名思义就是用来加载Class的。它负责将Class的字节码形式转换成内存形式的Class对象。

类的加载方式比较灵活,我们最常用的加载方式有下面几种:

  • 一种是根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容;
  • 另一种是从jar文件中读取
  • 从网络中获取,比如早期的Applet
  • 基于字节码生成技术生成的代理类

字节码的本质就是一个字节数组(byte[]),它有特定的复杂的内部格式。因为字节码文件有一定的格式,而且由ClassLoader进行加载,那么我们其实可以通过定制ClassLoader来实现字节码加密,原理很简单:

  • 加密:对java源代码进行编译得到字节码文件,然后使用某种算法对字节码文件进行加密
  • 解密:定制的ClassLoader会先使用加密算法对应的解密算法对加密的字节码文件进行解密,然后使用在正常加载jvm标准的字节码格式文件。

3个重要的ClassLoader

在上面的JVM架构图中,我们可以看到在类的加载阶段有3个重要的ClassLoader,下面分别介绍一下这3个比较重要的ClassLoader。

启动类加载器(BootstrapClassLoader)

这个类加载器负责加载JVM运行时核心类, 将<JAVA_HOME>\lib目录下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到虚拟机内存中,这个 ClassLoader比较特殊,它是由C/C++代码实现的,我们将它称之为「根加载器」。此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调用。

注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

扩展类加载器(ExtensionClassLoader)

这个类加载器sun.misc.Launcher$ExtClassLoader由Java语言实现的,是Launcher的静态内部类, 它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用使用这个类加载器。

常见的比如 swing 系列、内置的 js 引擎、xml 解析器等等都是由这个类加载器加载的, 这些库名通常以javax开头,它们的jar包位于<JAVA_HOME>\lib\ext目录下的类库。

//ExtClassLoader类中获取路径的代码
private static File[] getExtDirs() {
     //加载<JAVA_HOME>/lib/ext目录中的类库
     String s = System.getProperty("java.ext.dirs");
     File[] dirs;
     if (s != null) {
         StringTokenizer st =
             new StringTokenizer(s, File.pathSeparator);
         int count = st.countTokens();
         dirs = new File[count];
         for (int i = 0; i < count; i++) {
             dirs[i] = new File(st.nextToken());
         }
     } else {
         dirs = new File[0];
     }
     return dirs;
 }

应用程序类加载器(AppClassLoader)

sun.misc.Launcher$AppClassLoader才是直接面向我们用户的加载器,它负责加载系统类路径java -classpath-Djava.class.path指定路径下的类库,也就是我们经常用到的classpath路径jar包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。开发者可以直接使用系统类加载器, 这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值, 所以也称为系统类加载器.一般情况下这就是系统默认的类加载器. 当我们的 main 方法执行的时候,这第一个用户类的加载器就是AppClassLoader

那些位于网络上静态文件服务器提供的jar包和class文件,jdk 内置了一个URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。

ExtensionClassLoaderAppClassLoader都是URLClassLoader的子类,它们都是从本地文件系统里加载类库。

ClassLoader之间的层级关系

public abstract class Class {

   // Initialized in JVM not by private constructor
   // This field is filtered from reflection access, i.e. getDeclaredField
   // will throw NoSuchFieldException
   private final ClassLoader classLoader;
   }
public abstract class ClassLoader {
   // The parent class loader for delegation
   // Note: VM hardcoded the offset of this field, thus all new fields
   // must be added *after* it.
   private final ClassLoader parent;
   }

我们翻看jdk的代码会发现:

  • ClassLoader是一个抽象类
  • 每一个ClassLoader都有一个父ClassLoader的引用
  • 每一个Class中都有一个标记自己是哪个ClassLoader加载的属性

我们编写下面的测试代码:

public class TestClassLoader {

    public static void main(String[] args) {
        ClassLoader loader = TestClassLoader.class.getClassLoader();
        System.out.println(loader.toString());
        System.out.println(loader.getParent().toString());
        System.out.println(loader.getParent().getParent());
    }
}

输出结果:

sun.misc.Launcher$AppClassLoader@500c05c2
sun.misc.Launcher$ExtClassLoader@454e2c9c
null

从日志输出我们可以看出,我们的TestClassLoader是由AppClassLoader加载的,AppClassLoader的父ClassLoader是ExtClassLoader,而ExtClassLoader的 父ClassLoader是null,jvm约定当ClassLoader#getParent()返回时null的话,就默认使用启动类加载器作为父加载器.下面是ClassLoader.java中的关于getParent方法的描述:

    /**
     * Returns the parent class loader for delegation. Some implementations may
     * use <tt>null</tt> to represent the bootstrap class loader. This method
     * will return <tt>null</tt> in such implementations if this class loader's
     * parent is the bootstrap class loader.
     *
     * <p> If a security manager is present, and the invoker's class loader is
     * not <tt>null</tt> and is not an ancestor of this class loader, then this
     * method invokes the security manager's {@link
     * SecurityManager#checkPermission(java.security.Permission)
     * <tt>checkPermission</tt>} method with a {@link
     * RuntimePermission#RuntimePermission(String)
     * <tt>RuntimePermission("getClassLoader")</tt>} permission to verify
     * access to the parent class loader is permitted.  If not, a
     * <tt>SecurityException</tt> will be thrown.  </p>
     *
     * @return  The parent <tt>ClassLoader</tt>
     *
     * @throws  SecurityException
     *          If a security manager exists and its <tt>checkPermission</tt>
     *          method doesn't allow access to this class loader's parent class
     *          loader.
     *
     * @since  1.2
     */
    @CallerSensitive
    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;
    }

因此我们可以给出ClassLoader的继承关系图:

双亲委派模型

程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?

虚拟机的策略是: 使用调用者Class对象的ClassLoader来加载当前未知的类。

何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class对象。前面我们提到每个Class对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。

但是在加载的过程中,并不是直接加载的,而是会有一个层级查找关系在,这也就是所谓的「双亲委派模型」。

我们可以看一下ClassLoader的源代码来确认这一点:

 /**
     * Loads the class with the specified <a href="#name">binary name</a>.
     * This method searches for classes in the same manner as the {@link
     * #loadClass(String, boolean)} method.  It is invoked by the Java virtual
     * machine to resolve class references.  Invoking this method is equivalent
     * to invoking {@link #loadClass(String, boolean) <tt>loadClass(name,
     * false)</tt>}.
     *
     * @param  name
     *         The <a href="#name">binary name</a> of the class
     *
     * @return  The resulting <tt>Class</tt> object
     *
     * @throws  ClassNotFoundException
     *          If the class was not found
     */
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    
     /**
     * Loads the class with the specified <a href="#name">binary name</a>.  The
     * default implementation of this method searches for classes in the
     * following order:
     *
     * <ol>
     *
     *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
     *   has already been loaded.  </p></li>
     *
     *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
     *   on the parent class loader.  If the parent is <tt>null</tt> the class
     *   loader built-in to the virtual machine is used, instead.  </p></li>
     *
     *   <li><p> Invoke the {@link #findClass(String)} method to find the
     *   class.  </p></li>
     *
     * </ol>
     *
     * <p> If the class was found using the above steps, and the
     * <tt>resolve</tt> flag is true, this method will then invoke the {@link
     * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
     *
     * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
     * #findClass(String)}, rather than this method.  </p>
     *
     * <p> Unless overridden, this method synchronizes on the result of
     * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
     * during the entire class loading process.
     *
     * @param  name
     *         The <a href="#name">binary name</a> of the class
     *
     * @param  resolve
     *         If <tt>true</tt> then resolve the class
     *
     * @return  The resulting <tt>Class</tt> object
     *
     * @throws  ClassNotFoundException
     *          If the class could not be found
     */
    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;
        }
    }

从上面的代码我们就可以看到protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException函数实现了「双亲委派」。简单描述如下:

  • 检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
  • 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
  • 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。

换句话说,如果自定义类加载器,就必须重写findClass方法!

「双亲委派模型」是一种组织类加载器之间关系的一种规范,他的工作原理是: 如果一个类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样层层递进,最终所有的加载请求都被传到最顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,才会交给子类加载器去尝试加载.

从上面的分析我们可以知道:一般情况下,我们编写的java代码所有延迟加载的类都会由初始调用main方法的这个ClassLoader全全负责,它就是AppClassLoader

为什么需要双亲委派模型

比如java.lang.Object,它存放在\jre\lib\rt.jar中,它是所有java类的父类,因此无论哪个类加载都要加载这个类,最终所有的加载请求都汇总到顶层的启动类加载器中,因此Object类会由启动类加载器来加载,所以加载的都是同一个类,如果不使用双亲委派模型,由各个类加载器自行去加载的话,系统中就会出现不止一个Object类,应用程序就会全乱了

因为在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。

举个简单例子:

ClassLoader1、ClassLoader2都加载java.lang.String类,对应Class1、Class2对象。那么Class1对象不属于ClassLoad2对象加载的java.lang.String类型。

这样的好处是: java类随着它的类加载器一起具备了带有优先级的层次关系。

双亲委派规则可能会变成三亲委派,四亲委派,取决于你使用的父加载器是谁,它会一直递归委派到根加载器。只是一般我们习惯称为「双亲委派」。

延迟加载

JVM具体什么加载类,需要按照jvm的实现来说的。不过我们平时用的Hotspot虚拟机,运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。

ClassLoader的相关核心方法

loadClass()

loadClass()方法是加载目标类的入口,在这个方法内部实现了「双亲委派模型」。它首先会查找当前 ClassLoader以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用findClass() 让自定义加载器自己来加载目标类。ClassLoader 的findClass()方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用defineClass()方法将字节码转换成Class对象。

下面这个图还是画的比较形象的:

ClassLoader.loadClass()这是一个实例方法,需要一个ClassLoader对象来调用该方法,该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化.该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器.

ClassLoader cl= …….;
cl.loadClass(com.wang.HelloWorld);

提到这个ClassLoader.loadClass()方法,一般就需要提一下Class类的forName方法。

Class.forname()

Class.forname():是一个静态方法, 根据传入的类的全限定名返回一个Class对象.该方法在将Class文件加载到内存的同时,会执行类的初始化:

/**
     * Returns the {@code Class} object associated with the class or
     * interface with the given string name.  Invoking this method is
     * equivalent to:
     *
     * <blockquote>
     *  {@code Class.forName(className, true, currentLoader)}
     * </blockquote>
     *
     * where {@code currentLoader} denotes the defining class loader of
     * the current class.
     *
     * <p> For example, the following code fragment returns the
     * runtime {@code Class} descriptor for the class named
     * {@code java.lang.Thread}:
     *
     * <blockquote>
     *   {@code Class t = Class.forName("java.lang.Thread")}
     * </blockquote>
     * <p>
     * A call to {@code forName("X")} causes the class named
     * {@code X} to be initialized.
     *
     * @param      className   the fully qualified name of the desired class.
     * @return     the {@code Class} object for the class with the
     *             specified name.
     * @exception LinkageError if the linkage fails
     * @exception ExceptionInInitializerError if the initialization provoked
     *            by this method fails
     * @exception ClassNotFoundException if the class cannot be located
     */
    @CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

比如当我们在使用jdbc驱动时,经常会使用 Class.forName 方法来动态加载驱动类。

Class.forName("com.mysql.cj.jdbc.Driver");

其原理是 mysql 驱动的Driver类里有一个静态代码块,它会在 Driver 类被加载的时候执行。这个静态代码块会将 mysql 驱动实例注册到全局的 jdbc 驱动管理器里。

class Driver {
  static {
    try {
       java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
       throw new RuntimeException("Can't register driver!");
    }
  }
  ...
}

forName方法同样也是使用调用者Class对象的ClassLoader来加载目标类。不过 forName还提供了多参数版本,可以指定使用哪个ClassLoader来加载:

/**
     * Returns the {@code Class} object associated with the class or
     * interface with the given string name, using the given class loader.
     * Given the fully qualified name for a class or interface (in the same
     * format returned by {@code getName}) this method attempts to
     * locate, load, and link the class or interface.  The specified class
     * loader is used to load the class or interface.  If the parameter
     * {@code loader} is null, the class is loaded through the bootstrap
     * class loader.  The class is initialized only if the
     * {@code initialize} parameter is {@code true} and if it has
     * not been initialized earlier.
     *
     * <p> If {@code name} denotes a primitive type or void, an attempt
     * will be made to locate a user-defined class in the unnamed package whose
     * name is {@code name}. Therefore, this method cannot be used to
     * obtain any of the {@code Class} objects representing primitive
     * types or void.
     *
     * <p> If {@code name} denotes an array class, the component type of
     * the array class is loaded but not initialized.
     *
     * <p> For example, in an instance method the expression:
     *
     * <blockquote>
     *  {@code Class.forName("Foo")}
     * </blockquote>
     *
     * is equivalent to:
     *
     * <blockquote>
     *  {@code Class.forName("Foo", true, this.getClass().getClassLoader())}
     * </blockquote>
     *
     * Note that this method throws errors related to loading, linking or
     * initializing as specified in Sections 12.2, 12.3 and 12.4 of <em>The
     * Java Language Specification</em>.
     * Note that this method does not check whether the requested class
     * is accessible to its caller.
     *
     * <p> If the {@code loader} is {@code null}, and a security
     * manager is present, and the caller's class loader is not null, then this
     * method calls the security manager's {@code checkPermission} method
     * with a {@code RuntimePermission("getClassLoader")} permission to
     * ensure it's ok to access the bootstrap class loader.
     *
     * @param name       fully qualified name of the desired class
     * @param initialize if {@code true} the class will be initialized.
     *                   See Section 12.4 of <em>The Java Language Specification</em>.
     * @param loader     class loader from which the class must be loaded
     * @return           class object representing the desired class
     *
     * @exception LinkageError if the linkage fails
     * @exception ExceptionInInitializerError if the initialization provoked
     *            by this method fails
     * @exception ClassNotFoundException if the class cannot be located by
     *            the specified class loader
     *
     * @see       java.lang.Class#forName(String)
     * @see       java.lang.ClassLoader
     * @since     1.2
     */
    @CallerSensitive
    public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
        Class<?> caller = null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // Reflective call to get caller class is only needed if a security manager
            // is present.  Avoid the overhead of making this call otherwise.
            caller = Reflection.getCallerClass();
            if (sun.misc.VM.isSystemDomainLoader(loader)) {
                ClassLoader ccl = ClassLoader.getClassLoader(caller);
                if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                    sm.checkPermission(
                        SecurityConstants.GET_CLASSLOADER_PERMISSION);
                }
            }
        }
        return forName0(name, initialize, loader, caller);
    }

通过这种形式的forName方法可以突破内置加载器的限制,通过使用自定类加载器允许我们自由加载其它任意来源的类库。根据ClassLoader的传递性,目标类库传递引用到的其它类库也将会使用自定义加载器加载。

Class.forNameClassLoader.loadClass都可以用来加载目标类,它们之间有一个小小的区别,那就是Class.forName()方法可以获取原生类型的Class,而ClassLoader.loadClass()则会报错:

Class<?> x = Class.forName("[I");
System.out.println(x);

x = ClassLoader.getSystemClassLoader().loadClass("[I");
System.out.println(x);

---------------------
class [I

Exception in thread "main" java.lang.ClassNotFoundException: [I

findClass()

在上面的「双亲委派模型」小节中,我们从ClassLoader类的源代码分析了,loadClass()方法在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的findeClass()函数, 这样就可以保证自定义的类加载器也符合「双亲委派」。

如果想实现自定义的ClassLoader,那么必须实现findClass()方法,而ClassLoader中的默认实现为直接抛出ClassNotFoundException异常:

  /**
     * Finds the class with the specified <a href="#name">binary name</a>.
     * This method should be overridden by class loader implementations that
     * follow the delegation model for loading classes, and will be invoked by
     * the {@link #loadClass <tt>loadClass</tt>} method after checking the
     * parent class loader for the requested class.  The default implementation
     * throws a <tt>ClassNotFoundException</tt>.
     *
     * @param  name
     *         The <a href="#name">binary name</a> of the class
     *
     * @return  The resulting <tt>Class</tt> object
     *
     * @throws  ClassNotFoundException
     *          If the class could not be found
     *
     * @since  1.2
     */
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

defineClass(byte[] b, int off, int len)

defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象。在ClassLoader中已实现该方法逻辑,通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用。

一般情况下,在自定义类加载器时,会直接覆盖ClassLoaderfindClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象,简单例子如下:

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对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。

关于java类的生命周期,如果不了解的话,建议看看之前的文章《Java类的生命周期》open in new window

resolveClass(Class≺?≻ c)

使用该方法可以使用类的Class对象创建完成也同时被解析open in new window

上述4个方法是ClassLoader类中的比较重要的方法,也是我们可能会经常用到的方法。

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

前面说过,ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现,并新增了URLClassPath类协助取得Class字节码流等功能,在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

class文件的显示加载与隐式加载的概念

  • 显示加载 指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)this.getClass().getClassLoader().loadClass()加载class对象。
  • 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。

自定义类加载器

下面写一个简单的自定义类加载的例子

首先我们编写一个简单的java类,这个类就是后面需要被我们的自定义类加载器加载的类:

package xyz.xkrivzooh;

public class HelloWorld {

	public void sayHello() {
		System.out.println("hello " + this.getClass().getClassLoader().toString());
	}
}

我们使用javac编译后,将得到的HelloWorld.class文件。此处我们想一下,如果我们把这个字节码文件放置在zai当前的项目中的话,那么根据「双亲委派模型」可知这个字节码文件将会被sun.misc.Launcher$AppClassLoader类加载器加载,为了让我们自定义的类加载器加载,我们把HelloWorld.class文件放入到其他目录。

~ » tree xyz
xyz
└── xkrivzooh
    └── HelloWorld.class

然后编写我们自定义的类加载器:

package xyz.xkrivzooh;

import java.io.File;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;

public class CustomerClassLoader extends ClassLoader {

	private final String classPath;

	public CustomerClassLoader(String classPath) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(classPath));
		this.classPath = classPath;
	}

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(name));
		try {
			String path = name.replaceAll("\\.", "/");
			byte[] bytes = Files.readAllBytes(Paths.get(classPath + File.separator + path + ".class"));
			return defineClass(name, bytes, 0, bytes.length);
		}
		catch (Exception e) {
			throw new ClassNotFoundException(e.getMessage(), e);
		}
	}

	public static void main(String[] args) throws Exception{
		CustomerClassLoader customerClassLoader = new CustomerClassLoader("/Users/rollenholt");
		Class<?> aClass = customerClassLoader.loadClass("xyz.xkrivzooh.HelloWorld");
		Object instance = aClass.newInstance();
		Method sayHello = aClass.getDeclaredMethod("sayHello", null);
		sayHello.invoke(instance, null);
	}
}

输出结果为

hello xyz.xkrivzooh.CustomerClassLoader@4d405ef7

从上的例子我们可以看出,我们自定义的类加载器运行是没问题的。

我们平时在自定义类加载器的时候需要注意的是不要轻易的去破坏双亲委派模型,也就是不要去覆盖loadClass方法,除非你明确知道你在做什么

因为这样就可以导致导致自定义加载器无法加载内置的核心类库。在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入。如果父类加载器是 null,那就表示父加载器是「根加载器」BootstrapClassLoader

    /**
     * Creates a new class loader using the specified parent class loader for
     * delegation.
     *
     * <p> If there is a security manager, its {@link
     * SecurityManager#checkCreateClassLoader()
     * <tt>checkCreateClassLoader</tt>} method is invoked.  This may result in
     * a security exception.  </p>
     *
     * @param  parent
     *         The parent class loader
     *
     * @throws  SecurityException
     *          If a security manager exists and its
     *          <tt>checkCreateClassLoader</tt> method doesn't allow creation
     *          of a new class loader.
     *
     * @since  1.2
     */
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }

钻石依赖

项目管理上有一个著名的概念叫着「钻石依赖」,是指软件依赖导致同一个软件包的两个版本需要共存而不能冲突。

maven是这样解决钻石依赖的: 它会从多个冲突的版本中选择一个来使用,如果不同的版本之间兼容性很糟糕,那么程序将无法正常编译运行。Maven 这种形式叫「扁平化」依赖管理。

使用ClassLoader可以解决钻石依赖问题。不同版本的软件包使用不同的 ClassLoader 来加载,位于不同ClassLoader中名称一样的类实际上是不同的类。

我们通过下面的代码来验证这个问题:

首先准备下面的环境:

~/xyz/xkrivzooh » tree .
.
├── v1
│   ├── Test.class
│   └── Test.java
└── v2
    ├── Test.class
    └── Test.java

2 directories, 4 files
------------------------------------------------------------
~/xyz/xkrivzooh » cat v1/Test.java

public class Test {
	public void sayHello() {
		System.out.println("v1");
	}
}

------------------------------------------------------------
~/xyz/xkrivzooh » cat v2/Test.java

public class Test {
	public void sayHello() {
		System.out.println("v2");
	}
}

------------------------------------------------------------

然后使用测试代码:

package xyz.xkrivzooh;

import java.net.URL;
import java.net.URLClassLoader;

public class Test {
	public static void main(String[] args) throws Exception {

		String dir1 = "file:///Users/rollenholt/xyz/xkrivzooh/v1/";
		String dir2 = "file:///Users/rollenholt/xyz/xkrivzooh/v2/";
		URLClassLoader classLoader1 = new URLClassLoader(new URL[] {new URL(dir1)});
		URLClassLoader classLoader2 = new URLClassLoader(new URL[] {new URL(dir2)});

		Class<?> aClass1 = classLoader1.loadClass("Test");
		Object instance1 = aClass1.newInstance();
		aClass1.getDeclaredMethod("sayHello", null).invoke(instance1, null);

		Class<?> aClass2 = classLoader2.loadClass("Test");
		Object instance2 = aClass2.newInstance();
		aClass2.getDeclaredMethod("sayHello", null).invoke(instance2, null);

		System.out.println(aClass1.equals(aClass2));
		System.out.println(instance1.equals(instance2));


		URLClassLoader classLoader3 = new URLClassLoader(new URL[] {new URL(dir1)});
		Class<?> aClass3 = classLoader3.loadClass("Test");
		Object instance3 = aClass3.newInstance();
		aClass3.getDeclaredMethod("sayHello", null).invoke(instance3, null);

		System.out.println(aClass3.equals(aClass1));
		System.out.println(instance3.equals(instance1));
	}
}

程序运行输出:

v1
v2
false
false
v1
false
false

我们还可以让两个不同版本的Test类实现同一个接口,这样可以避免使用反射的方式来调用Test类里面的方法。

Class<?> aClass = classLoader1.loadClass("Test");
SomeInterface inter1 = (SomeInterface)aClass.getConstructor().newInstance();
inter1.sayHello()

ClassLoader固然可以解决依赖冲突问题,不过它也限制了不同软件包的操作界面必须使用反射或接口的方式进行动态调用。Maven没有这种限制,它依赖于虚拟机的默认懒惰加载策略,运行过程中如果没有显示使用定制的ClassLoader,那么从头到尾都是在使用AppClassLoader,而不同版本的同名类必须使用不同的ClassLoader加载,所以Maven不能完美解决钻石依赖。

蚂蚁金服开源的sofa-ark其实就是采用ClassLoader的方式来做类隔离的。

Thread.contextClassLoader

  /**
     * Returns the context ClassLoader for this Thread. The context
     * ClassLoader is provided by the creator of the thread for use
     * by code running in this thread when loading classes and resources.
     * If not {@linkplain #setContextClassLoader set}, the default is the
     * ClassLoader context of the parent Thread. The context ClassLoader of the
     * primordial thread is typically set to the class loader used to load the
     * application.
     *
     * <p>If a security manager is present, and the invoker's class loader is not
     * {@code null} and is not the same as or an ancestor of the context class
     * loader, then this method invokes the security manager's {@link
     * SecurityManager#checkPermission(java.security.Permission) checkPermission}
     * method with a {@link RuntimePermission RuntimePermission}{@code
     * ("getClassLoader")} permission to verify that retrieval of the context
     * class loader is permitted.
     *
     * @return  the context ClassLoader for this Thread, or {@code null}
     *          indicating the system class loader (or, failing that, the
     *          bootstrap class loader)
     *
     * @throws  SecurityException
     *          if the current thread cannot get the context ClassLoader
     *
     * @since 1.2
     */
    @CallerSensitive
    public ClassLoader getContextClassLoader() {
        if (contextClassLoader == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                   Reflection.getCallerClass());
        }
        return contextClassLoader;
    }

Thread.contextClassLoader「线程上下文类加载器」,从方法的描述我们可以知道,线程的contextClassLoader是从父线程那里继承过来的,所谓父线程就是创建了当前线程的线程。程序启动时的main线程的contextClassLoader 就是AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的contextClassLoader都是AppClassLoader。它可以做到跨线程共享类,只要它们共享同一个 contextClassLoader。父子线程之间会自动传contextClassLoader,所以共享起来将是自动化的。如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来。

总结

但是如果仅仅把ClassLoader当成一个将字节码形式的class转为内存形式的Class对象的工具的话有点狭义:他不仅仅是一个转换工具,他也相当于一个类的容器,或者叫命名空间可以起到「类隔离」的作用。位于同一个ClassLoader 里面的类名是唯一的,不同的ClassLoader可以持有同名的类。

同时通过「双亲委派模型」,不同的ClassLoader之间相互合作,形成一个层级关系。parent具有更高的加载优先级。除此之外,parent还表达了一种共享关系,当多个子ClassLoader共享同一个parent时,那么这个parent里面包含的类可以认为是所有子ClassLoader共享的。这也是为什么BootstrapClassLoader被所有的类加载器视为最顶层的加载器,JVM核心类库自然应该被共享。


版权申明

本站点所有内容,版权均归https://wenchao.renopen in new window所有,除非明确授权,否则禁止一切形式的转载协议

打赏

微信 支付宝

上次编辑于:
打赏
给作者赏一杯咖啡吧
您的支持将是我继续更新下去的动力
微信微信
支付宝支付宝