ClassLoader加载机制深入分析
阅读原文时间:2021年04月20日阅读:1

我们知道在java程序中,需要将我们写的java文件编译为class文件才能被使用,一个java程序也是由许许多多的class文件组成。在程序运行的时候是需要将这些class文件加载到内存中才能被我们使用的,而且这些class文件也不是一次性的被加载进内存的,它是什么时候需要就什么时候加载,加载这些class文件到内存中就需要使用ClassLoader类加载器来完成。一般情况下我们不需要关系ClassLoader,但是在一些特殊的情况下我们不得不去了解它们,比如如果我们需要从网络下加载一个class文件,或者从一些特殊的路径下加载class文件,那么就需要我们对ClassLoader有一个清晰的认识才能知道该如何下手。

一、默认类加载器

JDK提供了3个默认的类加载器:Bootstrap ClassLoaderExt ClassLoaderApp ClassLoader

  • BootstrapClassLoader:也叫引导类加载器,是用C++写的,不是继承java中的ClassLoader,在jvm启动的时候就会初始化它,它主要加载%JAVA_HOME%\jre\lib%JAVA_HOME%\jre\lib\classes以及-Xbootclasspath参数路径下的jar或者class。比如我们的String类等一些JDK提供的默认类都是由它来加载的。

我们可以使用如下代码来查看Boostrap加载器的加载路径:System.getProperty("sun.boot.class.path");

D:\DevTools\Java\jdk1.8.0_65\jre\lib\resources.jar
D:\DevTools\Java\jdk1.8.0_65\jre\lib\rt.jar
D:\DevTools\Java\jdk1.8.0_65\jre\lib\sunrsasign.jar
D:\DevTools\Java\jdk1.8.0_65\jre\lib\jsse.jar
D:\DevTools\Java\jdk1.8.0_65\jre\lib\jce.jar
D:\DevTools\Java\jdk1.8.0_65\jre\lib\charsets.jar
D:\DevTools\Java\jdk1.8.0_65\jre\lib\jfr.jar
D:\DevTools\Java\jdk1.8.0_65\jre\classes
  • ExtClassLoader:扩展类加器,它是用java代码实现的,主要加载%JAVA_HOME%\jre\lib\ext下的jar或者class。可以使用System.getProperty("java.ext.dirs")来查看它的加载路径

    D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext
    C:\Windows\Sun\Java\lib\ext
  • AppClassLoader:系统类加载器,主要加载应用程序中的类,可以使用System.getPropertyntege("java.class.path")来查看它的加载路径,使用不同的IDE,在不同的环境下该方法获取的路径可能会各不相同,比如我自己使用IntelliJ IDEA,执行结果如下:

    D:\DevTools\Java\jdk1.8.0_65\jre\lib\charsets.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\deploy.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\access-bridge-64.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\cldrdata.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\dnsns.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\jaccess.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\jfxrt.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\localedata.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\nashorn.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\sunec.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\sunjce_provider.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\sunmscapi.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\sunpkcs11.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\ext\zipfs.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\javaws.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\jce.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\jfr.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\jfxswt.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\jsse.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\management-agent.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\plugin.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\resources.jar
    D:\DevTools\Java\jdk1.8.0_65\jre\lib\rt.jar
    D:\workspace\Test\out\production\Test
    D:\workspace\Test\test\libs\fastjson-1.2.37.jar
    D:\DevTools\JetBrains\IntelliJ IDEA 2017.2.2\lib\idea_rt.jar

这3个类加载器在jvm启动的时候会被初始化,其中Bootstrap ClassLoader会先初始化来加载核心类库。我们可以看看sun.misc.Launcher这个类的构造方法:

//sun.misc.Launcher
public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        //初始化ExtClassLoader
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        //初始化AppClassLoader
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
    //设置线程上下文classLoader为AppClassLoader
    Thread.currentThread().setContextClassLoader(this.loader);
    String var2 = System.getProperty("java.security.manager");
    if (var2 != null) {
        ...
    }
}

在Launcher对象初始化的时候就会初始化ExtClassLoader和AppClassLoader,这两个类都是Launcher的内部类。

static class ExtClassLoader extends URLClassLoader {
static class AppClassLoader extends URLClassLoader {

我们看看这几个默认类加载器的继承关系:

可以看到除了BootstrapClassLoader在java中看不到,其他的类加载器都是直接或者间接继承ClassLoader的。其实它们的继承关系不是我们要重点关注的,我们关注的它们之间的一个父类加载器的一个关系。我们看看ClassLoader的构造方法。

private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    ...
}

在构造ClassLoader对象的时候需要传递一个额外的类加载器来作为父类加载器,为什么要使用父类加载器与类的加载机制有关,我们后面再详说。
那么各个类加载器之间的关系是什么呢?

一般情况下我们自己定义的类加载器的父类加载器就是AppClassLoader,而AppClassLoader的父类加载器是ExtClassLoader,ExtClassLoader的父类加载器是BootstrapClassLoader。
再回到Launcher的构造方法中来:

Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
    //设置线程上下文累加器为AppClassLoader
    Thread.currentThread().setContextClassLoader(this.loader);

发现在初始化AppClassLoader的时候将ExtClassLoader对象作为参数传给了AppClassLoader,从这里也就看到了为什么说AppClassLoader的父类加载器是ExtClassLoader。
那为什么说一般我们自己定义的ClassLoader的父类加载器的是AppClassLoader呢,还是回到ClassLoader的构造方法中来:

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}
public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    ...
    return scl;
}
private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            scl = l.getClassLoader();
            ...
        }
        sclSet = true;
    }
}

如果我们在构造ClassLodaer的时候不传递parent classloader的时候,会默认调用initSystemClassLoader来初始化一个父类加载器,而这个默认的父类加载器就是Launcher.getLauncher().getClassLoader,看看这里面是什么

public Launcher{
    ...
    this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    ...
}
public ClassLoader getClassLoader() {
    return this.loader;
}

发现这个默认的classloader就是AppClassLoader。

我们用代码来验证一下:

ClassLoader classLoader = Test.class.getClassLoader();
System.out.println(classLoader);
while (classLoader != null) {
    classLoader = classLoader.getParent();
    System.out.println(classLoader);
}

打印如下:

sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$ExtClassLoader@74a14482
null

返回null是因为BootstrapClassLoader是C++写的,java无法直接拿到。

二、双亲委托机制

如果需要加载一个class文件,我们需要调用ClassLoader.loadClass()方法。我们来看看这个方法里面具体做了什么

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
    synchronized (getClassLoadingLock(name)) {
        // 1.判断要加载的类是否已经被加载了
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //2-1.如果没有加载,并且父类加载器不为null,委托父类加载器去加载该类
                    c = parent.loadClass(name, false);
                } else {
                    //2-2.如果父类加载器为null,让Bootstrap加载器去加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                //3.如果父类加载器没有找到该类,那么就自己来加载
                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;
    }
}

loadClass的过程很简单,具体看注释就明白了。
双亲委托加载机制:当使用类加载器加载一个类的时候,该类加载器会先判断该类是否被加载了,如果已经加载了就直接返回;如果没有加载那么将在加载请求委托给父类加载器,同样父类加载器也会先判断该类是否被加载了,如果已经加载了就返回,如果没有那么父类加载器继续委托它的父类加载器去加载,这样最终该加载请求到达了BootstrapClassLoader,如果BootstrapClassLoader成功加载,那么就返回加载的class对象,如果它也没有找到,那么就按照刚刚委托向上的这个路线,向下返回给下一个类加载器去加载,依次最后返回到发送委托请求的类加载器那里,如果还是没有加载成功,那么就由它自己来加载,如果它也没有找到,就抛出ClassNotFoundException异常。

为什么ClassLoader要使用这种委托机制来实现类的加载呢?

判断一个类是不是相同除了判断它们是不是同一个class文件外,还需要判断这个class文件是不是被同一个ClassLoader加载。例如:我们自己的一个Test类,正常来说被AppClassLoader加载,如果我们使用自定义的类加载器来加载这个Test类,加载的class对象会是一样的吗?答案肯定是不一样的

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

        Class<Launcher> launcherClass = Launcher.class;
        try {
            //获取Launcher中的loader对象,它是一个AppClassLoader
            Field loaderField = launcherClass.getDeclaredField("loader");
            Field launcherField = launcherClass.getDeclaredField("launcher");
            launcherField.setAccessible(true);
            //先获取到一个launcher对象,在Launcher类中是一个静态成员
            Object launcher = launcherField.get(null);
            loaderField.setAccessible(true);
            //launcher中的appClassLoader对象
            Object appClassLoader = loaderField.get(launcher);
            //获取AppClassLoader.getAppClassLoader静态方法重新创建一个新的appClassLoader对象
            Class<?> loaderClass = appClassLoader.getClass();
            Method getAppClassLoaderMethod = loaderClass.getMethod("getAppClassLoader", ClassLoader.class);
            getAppClassLoaderMethod.setAccessible(true);

            //需要获取原来默认的appclassloader的parent,在构造新的appclassloader的时候需要一个父类加载器
            Method getParentMethod = loaderClass.getMethod("getParent");
            Object parentClassLoader = getParentMethod.invoke(appClassLoader);
            //创建了一个新的appclassloader
            Object newAppClassLoader = getAppClassLoaderMethod.invoke(null,parentClassLoader);

            //获取loadClass方法
            Method loadClassMethod = loaderClass.getMethod("loadClass", String.class, boolean.class);
            loadClassMethod.setAccessible(true);
            //使用新创建的AppClassLoader来加载当前的Test类,当前的Test没有写包名,所以直接写Test就可以了
            Object testClass = loadClassMethod.invoke(newAppClassLoader, "Test", false);
            System.out.println(testClass);
            System.out.println(Test.class);
            //判断两个Test.class是否相等
            System.out.println(Test.class.equals(testClass));
            //验证新加载的Test.class创建的对象能否强转为当前默认的Test.class
            Test t = (Test) ((Class)testClass).newInstance();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

我们的验证思路如下:一般来说我们自己写的java类,都是由AppClassLoader来加载的,因此我再重新创建一个新的AppClassLoader对象来和默认的AppClassLoader来加载同一个类,比如这里我们都来加载同一个Test.class,因为AppClassLoader我们无法直接访问到,所以通过反射我们在Launcher中找到AppClassLoader,然后创建新的AppClassLoader对象,接着使用这个新的AppClassLoader对象来加载Test,然后和当前已经加载的Test.class进行比较。我们看打印结果:

class Test
class Test
false
Exception in thread "main" java.lang.ClassCastException: Test cannot be cast to Test
    at Test.main(Test.java:52)

从结果可以看到使用两个类加载器加载进来的Test是不一样的,通过它们创建的对象也不能进行强转,否则抛出ClassCastException异常。

为了保证同一个java类在jvm中只存在一个Class,所以需要这种委托机制,是谁加载的某个类,不管使用哪个classloader,以后都让最开始的classloader来加载,如果同一个类被多个classloader加载,那不就出乱子了吗。正如上面的例子所以,同一个类被两个加载器加载进来,在使用的时候就报ClassCastException异常,这明显是不行的,所以在不强行使用恶意手段的时候,委托机制能够保证我们使用的都是同一个Class。

三、自定义ClassLoader

使用自定义的ClassLoader,我们可以加载文件系统中其他位置的class,甚至是网络上的class,为了保证class文件的安全性,我们还可以通过将class文件加密,在加载的过程的解密class文件来加载类,想怎么加载我们都是可以定制的。
一般来说,自定义ClassLoader的步骤很简单

  1. 继承ClassLoader类
  2. 覆写findClass方法
  3. 调用defineClass方法将字节数据转换为Class对象。

其中我们主要的工作就是在findClass中完成。

在ClassLoader中与加载类的几个核心方法有这么几个:loadClass()findLoadedClass()findClass()defineClass(),其中loadClass()是我们加载类的入口,里面已经做了委托机制的处理,所以不建议去覆写loadClass()方法,findLoadedClass()是用来检查Class是否已经被加载了,所以是不需要我们重写的,findClass()就是ClassLoader真正去加载类的核心实现,我们需要来覆写该方法,defineClass()是用来将字节数组转为Class对象的方法,我们也不需要覆写此方法,我们在加载Class文件后需要调用此方法来转为Class对象。
现在我们来自定义个从D:\test目录下加载class的自定义加载器

public class CustomClassLoader extends ClassLoader {
    private String rootPath;

    public CustomClassLoader(String rootPath) {
        this.rootPath = rootPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = loadData(name);
        if (data != null) {
            return defineClass(name,data,0,data.length);
        }
        return null;
    }

    private byte[] loadData(String name) {
        String classPath = generateClassName(name);
        try {
            InputStream in = new FileInputStream(new File(rootPath,classPath));
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            byte[] buf = new byte[1024];
            int len;
            while ((len = in.read(buf)) != -1) {
                out.write(buf,0,len);
            }
            byte[] bytes = out.toByteArray();
            in.close();
            out.close();
            return bytes;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoClassDefFoundError e) {
            e.printStackTrace();
        }
        return null;
    }

    private String generateClassName(String name) {
        return name.replaceAll("\\.","/")+".class";
    }
}

测试类:

public class Demo {
    public static void main(String[] args) {
        CustomClassLoader customClassLoader = new CustomClassLoader("D:\\test");
        try {
            Class<?> loadTest = customClassLoader.loadClass("Test");
            Object test = loadTest.newInstance();
            Method print = loadTest.getMethod("print");
            print.invoke(test);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

我们将Test.class放在我们自定义的目录下,该类中有一个print()方法:

public void print(){
    System.out.println("test class loader");
}

我们看打印结果:

通过自定义的ClassLoader,我们成功的加载了给定目录下的class。