深刻分析Java类加载器原理

本文分析了双亲委派模型的实现原理,并经过代码示例说明了何时须要实现本身的类加载器以及如何实现本身的类加载器。java

本文基于JDK8。web

0 ClassLoader的做用

ClassLoader用于将class文件加载到JVM中。另一个做用是确认每一个类应该由哪一个类加载器加载。bash

第二个做用也用于判断JVM运行时的两个类是否相等,影响的判断方法有equals()、isAssignableFrom()、isInstance()以及instanceof关键字,这一点在后文中会举例说明。微信

0.1 什么时候出发类加载动做?

类加载的触发能够分为隐式加载和显示加载。网络

隐式加载框架

隐式加载包括如下几种状况:dom

  • 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时
  • 对类进行反射调用时
  • 当初始化一个类时,若是其父类尚未初始化,优先加载其父类并初始化
  • 虚拟机启动时,需指定一个包含main函数的主类,优先加载并初始化这个主类

显示加载函数

显示加载包含如下几种状况:post

  • 经过ClassLoader的loadClass方法
  • 经过Class.forName
  • 经过ClassLoader的findClass方法

0.2 被加载的类存放在哪里

JDK8以前会加载到内存中的方法区。 从JDK8到如今为止,会加载到元数据区。ui

1 都有哪些ClassLoader

整个JVM平台提供三类ClassLoader。

1.1 Bootstrap ClassLoader

加载JVM自身工做须要的类,它由JVM本身实现。它会加载$JAVA_HOME/jre/lib下的文件

1.2 ExtClassLoader

它是JVM的一部分,由sun.misc.LauncherExtClassLoader实现,他会加载JAVA_HOME/jre/lib/ext目录中的文件(或由System.getProperty("java.ext.dirs")所指定的文件)。

1.3 AppClassLoader

应用类加载器,咱们工做中接触最多的也是这个类加载器,它由sun.misc.Launcher$AppClassLoader实现。它加载由System.getProperty("java.class.path")指定目录下的文件,也就是咱们一般说的classpath路径。

2 双亲委派模型

2.1 双亲委派模型原理

从JDK1.2以后,类加载器引入了双亲委派模型,其模型图以下:

ClassLoaderParentMod

其中,两个用户自定义类加载器的父加载器是AppClassLoader,AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader是没有父类加载器的,在代码中,ExtClassLoader的父类加载器为null。BootstrapClassLoader也并无子类,由于他彻底由JVM实现。

双亲委派模型的原理是:当一个类加载器接收到类加载请求时,首先会请求其父类加载器加载,每一层都是如此,当父类加载器没法找到这个类时(根据类的全限定名称),子类加载器才会尝试本身去加载。

为了说明这个继承关系,我这里实现了一个本身的类加载器,名为TestClassLoader,在类加载器中,用parent字段来表示当前加载器的父类加载器,其定义以下:

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;
...
}
复制代码

而后经过debug来看一下这个结构,以下图

ClassLoader委派模型关系图

这里的第一个红框是我本身定义的类加载器,对应上图的最下层部分;第二个框是自定义类加载器的父类加载器,能够看到是AppClassLoader;第三个框是AppClassLoader的父类加载器,是ExtClassLaoder;第四个框是ExtClassLoader的父类加载器,是null。

OK,这里先有个直观的印象,后面的实现原理中会详细介绍。

2.2 此模型解决的问题

为何要使用双亲委派模型呢?它能够解决什么问题呢?

双亲委派模型是JDK1.2以后引入的。根据双亲委派模型原理,能够试想,没有双亲委派模型时,若是用户本身写了一个全限定名为java.lang.Object的类,并用本身的类加载器去加载,同时BootstrapClassLoader加载了rt.jar包中的JDK自己的java.lang.Object,这样内存中就存在两份Object类了,此时就会出现不少问题,例如根据全限定名没法定位到具体的类。

有了双亲委派模型后,全部的类加载操做都会优先委派给父类加载器,这样一来,即便用户自定义了一个java.lang.Object,但因为BootstrapClassLoader已经检测到本身加载了这个类,用户自定义的类加载器就不会再重复加载了。

因此,双亲委派模型可以保证类在内存中的惟一性。

2.3 双亲委派模型实现原理

下面从源码的角度看一下双亲委派模型的实现。

JVM在加载一个class时会先调用classloader的loadClassInternal方法,该方法源码以下

// This method is invoked by the virtual machine to load a class.
private Class<?> loadClassInternal(String name)
    throws ClassNotFoundException
{
    // For backward compatibility, explicitly lock on 'this' when
    // the current class loader is not parallel capable.
    if (parallelLockMap == null) {
        synchronized (this) {
             return loadClass(name);
        }
    } else {
        return loadClass(name);
    }
}
复制代码

该方法里面作的事儿就是调用了loadClass方法,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 {
                    // 若是父类加载器为null,说明ExtClassLoader也没有找到目标类,则调用BootstrapClassLoader来查找
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            // 若是都没有找到,调用findClass方法,尝试本身加载这个类
            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;
    }
}
复制代码

源码中已经给出了几个关键步骤的说明。

代码中调用BootstrapClassLoader的地方实际是调用的native方法。

因而可知,双亲委派模型实现的核心就是这个loadClass方法。

3 实现本身的类加载器

3.1 为何要实现本身的类加载器

回答这个问题首先要思考类加载器有什么做用(粗体标出)。

3.1.1 类加载器的做用

类加载器有啥做用呢?咱们再回到上面的源码。

从上文咱们知道JVM经过loadClass方法来查找类,因此,他的第一个做用也是最重要的:在指定的路径下查找class文件(各个类加载器的扫描路径在上文已经给出)。

而后,当父类加载器都说没有加载过目标类时,他会尝试本身加载目标类,这就调用了findClass方法,能够看一下findClass方法的定义:

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

能够发现他要求返回一个Class对象实例,这里我经过一个实现类sun.rmi.rmic.iiop.ClassPathLoader来讲明一下findClass都干了什么。

protected Class findClass(String var1) throws ClassNotFoundException {
    // 从指定路径加载指定名称的class的字节流
    byte[] var2 = this.loadClassData(var1);
    // 经过ClassLoader的defineClass来建立class对象实例
    return this.defineClass(var1, var2, 0, var2.length);
}
复制代码

他作的事情在注释中已经给出,能够看到,最终是经过defineClass方法来实例化class对象的。

另外能够发现,class文件字节的获取和处理咱们是能够控制的。因此,第二个做用:咱们能够在字节流解析这一步作一些自定义的处理。 例如,加解密。

接下来,看似还有个defineClass可让咱们来作点儿什么,ClassLoader的实现以下:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    throws ClassFormatError
{
    return defineClass(name, b, off, len, null);
}
复制代码

被final掉了,没办法覆写,因此这里看似不能作什么事儿了。

小结一下:

  • 经过loadClass在指定的路径下查找文件。
  • 经过findClass方法解析class字节流,并实例化class对象。
3.1.2 何时须要本身实现类加载器

当JDK提供的类加载器实现没法知足咱们的需求时,才须要本身实现类加载器。

现有应用场景:OSGi、代码热部署等领域。

另外,根据上述类加载器的做用,可能有如下几个场景须要本身实现类加载器

  • 当须要在自定义的目录中查找class文件时(或网络获取)
  • class被类加载器加载前的加解密(代码加密领域)

3.2 如何实现本身的类加载器

接下来,实现一个在自定义class类路径中查找并加载class的自定义类加载器。

package com.lordx.sprintbootdemo.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

/**
 * 自定义ClassLoader
 * 功能:可自定义class文件的扫描路径
 * @author zhiminxu 
 */
// 继承ClassLoader,获取基础功能
public class TestClassLoader extends ClassLoader {

    // 自定义的class扫描路径
    private String classPath;

    public TestClassLoader(String classPath) {
        this.classPath = classPath;
    }

    // 覆写ClassLoader的findClass方法
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // getDate方法会根据自定义的路径扫描class,并返回class的字节
        byte[] classData = getDate(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            // 生成class实例
            return defineClass(name, classData, 0, classData.length);
        }
    }


    private byte[] getDate(String name) {
        // 拼接目标class文件路径
        String path = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int num = 0;
            while ((num = is.read(buffer)) != -1) {
                stream.write(buffer, 0 ,num);
            }
            return stream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
复制代码

使用自定义的类加载器

package com.lordx.sprintbootdemo.classloader;

public class MyClassLoader {
    public static void main(String[] args) throws ClassNotFoundException {
        // 自定义class类路径
        String classPath = "/Users/zhiminxu/developer/classloader";
        // 自定义的类加载器实现:TestClassLoader
        TestClassLoader testClassLoader = new TestClassLoader(classPath);
        // 经过自定义类加载器加载
        Class<?> object = testClassLoader.loadClass("ClassLoaderTest");
        // 这里的打印应该是咱们自定义的类加载器:TestClassLoader
        System.out.println(object.getClassLoader());
    }
}
复制代码

实验中的ClassLoaderTest类就是一个简单的定义了两个field的Class。以下图所示

classloadertest

最终的打印结果以下

Result

PS: 实验类(ClassLoaderTest)最好不要放在IDE的工程目录内,由于IDE在run的时候会先将工程中的全部类都加载到内存,这样一来这个类就不是自定义类加载器加载的了,而是AppClassLoader加载的。

3.3 类加载器对“相等”判断的影响

3.3.1 对Object.equals()的影响

仍是上面那个自定义类加载器

修改MyClassLoader代码

package com.lordx.sprintbootdemo.classloader;

public class MyClassLoader {
    public static void main(String[] args) throws ClassNotFoundException {
        // 自定义class类路径
        String classPath = "/Users/zhiminxu/developer/classloader";
        // 自定义的类加载器实现:TestClassLoader
        TestClassLoader testClassLoader = new TestClassLoader(classPath);
        // 经过自定义类加载器加载
        Class<?> object1 = testClassLoader.loadClass("ClassLoaderTest");
        Class<?> object2 = testClassLoader.loadClass("ClassLoaderTest");
        // object1和object2使用同一个类加载器加载时
        System.out.println(object1.equals(object2));

        // 新定义一个类加载器
        TestClassLoader testClassLoader2 = new TestClassLoader(classPath);
        Class<?> object3 = testClassLoader2.loadClass("ClassLoaderTest");
        // object1和object3使用不一样类加载器加载时
        System.out.println(object1.equals(object3));
    }
}

复制代码

打印结果:

true

false

equals方法默认比较的是内存地址,因而可知,不一样的类加载器实例都有本身的内存空间,即便类的全限定名相同,但类加载器不一样也是不行的。

因此,内存中两个类equals为true的条件是用同一个类加载器实例加载的全限定名相同的两个类实例。

3.3.2 对instanceof的影响

修改TestClassLoader,增长main方法来实验,修改后的TestClassLoader以下:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

/**
 * 自定义ClassLoader
 * 功能:可自定义class文件的扫描路径
 * @author zhiminxu
 */
// 继承ClassLoader,获取基础功能
public class TestClassLoader extends ClassLoader {

    public static void main(String[] args) throws ClassNotFoundException {
        TestClassLoader testClassLoader = new TestClassLoader("/Users/zhiminxu/developer/classloader");
        Object obj = testClassLoader.loadClass("ClassLoaderTest");
        // obj是testClassLoader加载的,ClassLoaderTest是AppClassLoader加载的,因此这里打印:false
        System.out.println(obj instanceof ClassLoaderTest);
    }

    // 自定义的class扫描路径
    private String classPath;

    public TestClassLoader(String classPath) {
        this.classPath = classPath;
    }

    // 覆写ClassLoader的findClass方法
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // getDate方法会根据自定义的路径扫描class,并返回class的字节
        byte[] classData = getDate(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            // 生成class实例
            return defineClass(name, classData, 0, classData.length);
        }
    }


    private byte[] getDate(String name) {
        // 拼接目标class文件路径
        String path = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int num = 0;
            while ((num = is.read(buffer)) != -1) {
                stream.write(buffer, 0 ,num);
            }
            return stream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
复制代码

打印结果:

false

其余的还会影响Class的isAssignableFrom()方法和isInstance()方法,缘由与上面的相同。

3.3.3 另外的说明

不要尝试自定义java.lang包,并尝试用加载器去加载他们。像下面这样

selfjavalang

这么干的话,会直接抛出一个异常

error

这个异常是在调用defineClass的校验过程抛出的,源码以下

// Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
    throw new SecurityException
        ("Prohibited package name: " +
         name.substring(0, name.lastIndexOf('.')));
}
复制代码

4 跟ClassLoader相关的几个异常

想知道以下几个异常在什么状况下抛出,其实只须要在ClassLoader中找到哪里会抛出他,而后在看下相关逻辑便可。

4.1 ClassNotFoundException

这个异常,相信你们常常遇到。

那么,到底啥缘由致使抛出这个异常呢?

看一下ClassLoader的源码,在JVM调用loadClassInternal的方法中,就会抛出这个异常。

其声明以下:

// This method is invoked by the virtual machine to load a class.
private Class<?> loadClassInternal(String name)
    throws ClassNotFoundException
{
    // For backward compatibility, explicitly lock on 'this' when
    // the current class loader is not parallel capable.
    if (parallelLockMap == null) {
        synchronized (this) {
             return loadClass(name);
        }
    } else {
        return loadClass(name);
    }
}
复制代码

这里面loadClass方法会抛出这个异常,再来看loadClass方法

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
复制代码

调用了重写的loadClass方法

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 查看findLoadedClass的声明,没有抛出这个异常
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 这里会抛,但一样是调loadClass方法,无需关注
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // catch住没有抛,由于要在下面尝试本身获取class
                // 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;
    }
}
复制代码

再来看findClass方法

/**
 * 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);
}
复制代码

果真这里抛出的,注释中抛出这个异常的缘由是说:当这个class没法被找到时抛出。

这也就是为何在上面自定义的类加载器中,覆写findClass方法时,若是没有找到class要抛出这个异常的缘由。

至此,这个异常抛出的缘由就明确了:在双亲委派模型的全部相关类加载器中,目标类在每一个类加载器的扫描路径中都不存在时,会抛出这个异常。

4.2 NoClassDefFoundError

这也是个常常会碰到的异常,并且不熟悉的同窗可能常常搞不清楚何时抛出ClassNotFoundException,何时抛出NoClassDefFoundError。

咱们仍是到ClassLoader中搜一下这个异常,能够发如今defineClass方法中可能抛出这个异常,defineClass方法源码以下:

/**
 * ... 忽略注释和参数以及返回值的说明,直接看异常声明
 * 
 * @throws  ClassFormatError
 *          If the data did not contain a valid class
 *
 * 在这里。因为NoClassDefFoundError是Error下的,因此不用显示throws
 * @throws  NoClassDefFoundError
 *          If <tt>name</tt> is not equal to the <a href="#name">binary
 *          name</a> of the class specified by <tt>b</tt>
 *
 * @throws  IndexOutOfBoundsException
 *          If either <tt>off</tt> or <tt>len</tt> is negative, or if
 *          <tt>off+len</tt> is greater than <tt>b.length</tt>.
 *
 * @throws  SecurityException
 *          If an attempt is made to add this class to a package that
 *          contains classes that were signed by a different set of
 *          certificates than this class, or if <tt>name</tt> begins with
 *          "<tt>java.</tt>".
 */
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}
复制代码

继续追踪里面的方法,能够发现是preDefineClass方法抛出的,这个方法源码以下:

/* Determine protection domain, and check that:
    - not define java.* class,
    - signer of this class matches signers for the rest of the classes in
      package.
*/
private ProtectionDomain preDefineClass(String name,
                                        ProtectionDomain pd)
{
    // 这里显示抛出,可发现是在目标类名校验不经过时抛出的
    if (!checkName(name))
        throw new NoClassDefFoundError("IllegalName: " + name);

    // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
    // relies on the fact that spoofing is impossible if a class has a name
    // of the form "java.*"
    if ((name != null) && name.startsWith("java.")) {
        throw new SecurityException
            ("Prohibited package name: " +
             name.substring(0, name.lastIndexOf('.')));
    }
    if (pd == null) {
        pd = defaultDomain;
    }

    if (name != null) checkCerts(name, pd.getCodeSource());

    return pd;
}
复制代码

这个校验的源码以下

// true if the name is null or has the potential to be a valid binary name
private boolean checkName(String name) {
    if ((name == null) || (name.length() == 0))
        return true;
    if ((name.indexOf('/') != -1)
        || (!VM.allowArraySyntax() && (name.charAt(0) == '[')))
        return false;
    return true;
}
复制代码

因此,这个异常抛出的缘由是:类名校验未经过。

但这个异常其实不止在ClassLoader中抛出,其余地方,例如框架中,web容器中都有可能抛出,还要具体问题具体分析。

5 总结

本文先介绍了ClassLoader的做用:主要用于从指定路径查找class并加载到内存,另外是判断两个类是否相等。

后面介绍了双亲委派模型,以及其实现原理,JDK中主要是在ClassLoader的loadClass方法中实现双亲委派模型的。

而后代码示例说明了如何实现本身的类加载器,以及类加载器的使用场景。

最后说明了在工做中常遇到的两个与类加载器相关的异常。


欢迎关注个人微信公众号

公众号
相关文章
相关标签/搜索