深入理解Java ClassLoader机制-持续更新

39 阅读3分钟

1.背景

Java和类加载有关的八股文想必大家都背的滚瓜烂熟了,什么双亲委派等等.但是对于其中的细节,你又了解多少,这篇文章来深入理解Java ClassLoader机制

2.重要知识点

2.1 类加载器的父子关系非java类的继承关系extends

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

从上面的代码块我们可以看到,AppClassLoader和ExtClassLoader类加载器都继承了URLClassLoader.那么为什么会说ExtClassLoader类加载器是AppClassLoader类加载器的父加载器呢?

URLClassLoader继承了SecureClassLoader, SecureClassLoader继承了ClassLoader抽象类,抽象类中有这样一个属性:

private final ClassLoader parent;

类加载器的基础关系如下图所示: image.png

那么什么时候将AppClassLoaderClassLoader parent属性进行设置的呢?首先从JVM入口应用sun.misc.Launcher聊起,看下面一段代码.

public Launcher() {
    ExtClassLoader localExtClassLoader;
    try {
        // 加载扩展类加载器
        localExtClassLoader = ExtClassLoader.getExtClassLoader();
    } catch (IOException localIOException1) {
        throw new InternalError("Could not create extension class loader", localIOException1);
    }
    try {
        // 加载应用类加载器
        this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
    } catch (IOException localIOException2) {
        throw new InternalError("Could not create application class loader", localIOException2);
    }
    // 设置AppClassLoader为线程上下文类加载器
    Thread.currentThread().setContextClassLoader(this.loader);
    // ...
    
    static class ExtClassLoader extends java.net.URLClassLoader
    static class AppClassLoader extends java.net.URLClassLoader
}

重点看这一行代码AppClassLoader.getAppClassLoader(localExtClassLoader);这里为什么初始化AppClassLoader时要传入ExtClassLoader实例?

答:初始化AppClassLoader时传入的ExtClassLoader实例最终设置为了AppClassLoader(ClassLoader)的parent属性.

2.2 [ClassLoader+类的全路径]会唯一确定一个类对象

看下面的一段代码

public static void main(String[] args) throws Throwable {
    // Person类的类全路径
    String className = "com.demo.example.Person";

    // myClazz是自定义为URLClassLoader加载器加载
    Class<?> myClazz = loadClass(new String[] { "file:/Users/zhangustb/Documents/code_projects/zhangustb_demo/zhangustb_demo-start/target/classes/" }, null, className);
    System.out.println(myClazz.getClassLoader());

    // appClazz1是系统类加载器appClassLoader加载
    Class<?> appClazz1 = Class.forName(className);

    // appClazz2是系统类加载器appClassLoader加载
    Class<?> appClazz2 = Class.forName(className);
    
    // 输出myClazz appClazz1 appClazz2三个Class对象的关系
    System.out.println("myClazz == appClazz1 ? " + myClazz.equals(appClazz1));
    System.out.println("appClazz1 == appClazz2 ? " + appClazz1.equals(appClazz2));

    // 注意这里会出现类型转换异常
    myClazz.newInstance();
}

public static Class<?> loadClass(String[] pathArray, ClassLoader parentClassLoader, String className) throws Throwable {
    List<URL> urlList = new ArrayList<>();
    for (String path : pathArray) {
        URL url = new URL(path);
        urlList.add(url);
    }

    URL[] urls = urlList.toArray(new URL[urlList.size()]);
    URLClassLoader classLoader = new URLClassLoader(urls, parentClassLoader);
    return classLoader.loadClass(className);
}

java.net.URLClassLoader@682a0b20
myClazz == appClazz1 ? false
appClazz1 == appClazz2 ? true
Exception in thread "main" java.lang.InstantiationException: com.demo.example.Person
	at java.lang.Class.newInstance(Class.java:427)
	at com.demo.example.TestZzx.main(TestZzx.java:29)
Caused by: java.lang.NoSuchMethodException: com.demo.example.Person.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.newInstance(Class.java:412)
	... 1 more

Process finished with exit code 1

从打印结果来看

Person类编译后在classpath目录下.
myClazz由自定义类加载器加载,
appClazz1、appClazz2由系统类加载器appClassLoader加载.

生成的myClazz与appClazz1两个class对象不相等,而appClazz1与appClazz2两对象是相等的。

这说明,相同路径下的.class文件,被不同类加载器加载到内存中会生成两个不同的Class对象。

2.3 Tomcat破坏双亲委派的原因

在初学时部署项目,我们是把war包放到tomcat的webapp下,这意味着一个tomcat可以运行多个Web应用程序.

那假设我现在有两个Web应用程序,它们都有一个类,叫做Person,并且它们的类全限定名都一样,比如都是com.demo.Person,但是Person类的实现逻辑是不同的.如果Tomcat没有破坏双亲委派机制,还是沿用Java的类加载机制,比如说应用程序A是先在Tomcat容器中部署运行了,Person类使用的时候会被appclassloader加载器进行加载.

此时应用程序B部署运行,那么加载Person类的时候,发现com.demo.Person已经被加载过了,会直接使用Person类.但是很明显使用的Person类是应用程序A的

所以Tomcat是如何保证它们是不会冲突的呢?

答案就是,Tomcat给每个 Web 应用创建一个类加载器实例WebAppClassLoader,该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找