深入openjdk源码全面理解Java类加载器(下 -- Java源码篇)

  • 时间:
  • 浏览:
  • 来源:互联网

目录

  • 前言
  • 一、双亲委派
    • 1.1 类加载器结构
    • 1.2 双亲委派
  • 二、自定义类加载器
    • 2.1 全盘委派
    • 2.2 覆盖核心类?
  • 三、TCCL
  • 四、spring的类加载

前言

  在深入openjdk源码全面理解Java类加载器(上 – JVM源码篇)我们分析了JVM是如何启动,并且初始化BootStrapClassLoader的,也提到了sun.misc.Launcher被加载后会创建ExtClassLoader和AppClassLoader。关于类加载的基础知识请参考虚拟机类加载机制(上)。这篇文章主要从Java源码层面总结一下双亲委派、TCCL的应用等,然后再聊聊自定义类加载器的注意事项。

一、双亲委派

1.1 类加载器结构

  直接在idea里看看AppClassLoader的继承关系(ExtClassLoader一样):
在这里插入图片描述
  AppClassLoader和ExtClassLoader都继承自URLClassLoader,URLClassLoader继承自SecureClassLoader,最终继承自ClassLoader。类加载的核心方法以private native定义在ClasssLoader中,只能由ClassLoader调用,所以所有的自定义类加载器都必须直接或间接继承ClassLoader。

1.2 双亲委派

  加载类的核心方法是loadClass,默认实现在ClassLoader中:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
        	//同步锁,可能是一个和name对应的Object,也可能是this
        	//取决于类加载器是否具备并行能力
            //首先检查类是否被本类加载器加载了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
            	//如果没有找到需要加载的类
                long t0 = System.nanoTime();
                try {
                	//使用父类加载器加载类
                	//如果parent不为null,说明设置了父加载器,直接用parent
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    	//如果parent为null,使用BootStrapClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    //如果父类加载器没能加载到类,使用本类加载器加载
                    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;
        }
    }

注:关于getClassLoadingLock,可参考:关于类加载的并发控制锁。

  从loadClass的逻辑中可以很清晰的看到双亲委派的实现:首先查看类是否已经加载,如果未加载则委派给父类加载,如果父类加载器没能加载成功,那么才由本类加载器加载。
  通常情况下,所有Java实现的类加载器都是调用ClassLoader的这个loadClass方法,所以本类加载和父类加载器都是这个逻辑:本类加载器委托父类加载器,父类加载器委托租父类加载器等等。顶层类加载器如果无法加载则依次回溯。

二、自定义类加载器

  自定义类加载器需要直接或间接继承ClassLoader,最简单的一个自定义类加载器就是继承ClassLoader,重写其findClass方法,通过ClassLoader.defineClass方法创建一个Class类(defineClass最终会调用ClassLoader的native方法):

public class MyClassLoader extends ClassLoader {
    private URLClassPath ucp;

    public MyClassLoader(String path, ClassLoader parent) throws Exception {
        super(parent);
        this.ucp = new URLClassPath(new URL[]{new File(path).toURI().toURL()});
    }

    static {
        ClassLoader.registerAsParallelCapable();
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String usePath = name.replace('.', File.separatorChar).concat(".class");
        Resource resource = ucp.getResource(usePath, false);
        if (resource != null) {
            try {
                byte[] bytes = resource.getBytes();
                return defineClass(name, bytes, 0, bytes.length);
            } catch (IOException var) {
                return null;
            }
        } else {
            return null;
        }
    }
}

  MyClassLoader从我们指定的路径搜寻类文件,如果没有找到,那么父类ClassLoader的加载逻辑会遵循双亲委派交给我们指定的父类加载器加载,若未指定,那么寻找BootStrapClassLoader。
  正是由于我们只重写了findClass方法,类加载的过程还是双亲委派的逻辑,这也是Java官方建议的自定义类加载的方式。但是如果我们需要打破双亲委派规则,就必须重写loadClass方法,比如:

public class MyClassLoader2 extends ClassLoader {
    private URLClassPath ucp;


    public MyClassLoader2(String path, ClassLoader parent) throws MalformedURLException {
        super(parent);
        this.ucp = new URLClassPath(new URL[]{new File(path).toURI().toURL()});
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        if (name.startsWith("com.demo")) {
            Resource resource = ucp.getResource(name.replace('.', File.separatorChar).concat(".class"), false);
            if (resource != null) {
                try {
                    byte[] bytes = resource.getBytes();
                    Class clazz = defineClass(name, bytes, 0, bytes.length);
                    if (resolve) {
                        resolveClass(clazz);
                    }
                    return clazz;
                } catch (IOException e) {
                    throw new ClassNotFoundException(e.getMessage());
                }

            } else {
                throw new ClassNotFoundException();
            }
        } else {
            return super.loadClass(name, resolve);
        }
    }
}

  这个类加载器对于com.demo包的类都由自己加载,其余的才委托给父类。测试一下:

public class JavaMain {
    public static void main(String[] args) throws Exception {
        String classPath = JavaMain.class.getClassLoader().getResource("").getPath();
        MyClassLoader2 myClassLoader = new MyClassLoader2(classPath, JavaMain.class.getClassLoader());
        Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false);
        System.out.println(clazz.getClassLoader());
    }
}
//输出 com.demo.classloader.MyClassLoader2@5cad8086

2.1 全盘委派

  在我们的这个工程中,有一个问题,如果运行以下代码:

public static void main(String[] args) throws Exception {
        String classPath = JavaMain.class.getClassLoader().getResource("").getPath();
        MyClassLoader2 myClassLoader = new MyClassLoader2(classPath, JavaMain.class.getClassLoader());
        Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false);
        System.out.println(clazz.getClassLoader());
        System.out.println(TestClass1.class.getClassLoader());
        TestClass1 testClass1 = (TestClass1) clazz.newInstance();
    }
    //输出:
    com.demo.classloader.MyClassLoader2@5cad8086
	sun.misc.Launcher$AppClassLoader@18b4aac2
	ClassCastException

  类型强转操作会抛出java.lang.ClassCastException异常。造成这个的原因已经在输出结果中体现了,clazz是由自定义类加载加载的,而TestClass1.class是由AppClassLoader加载的。可以打印看看AppClassLoaer的加载目录:

System.out.println(System.getProperty("java.class.path"));

  在mac下结果如下:

/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/tools.jar:/Users/loren/work/github/test/out/production/test:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar

  输出的目录包含了当前项目目录,所以目录中的class可以被AppClassLoader加载。

注:除了当前项目目录,还有很多系统jar包,包括rt.jar、jce.jar等,当然由于AppClassLoader遵循双亲委派,路径包含这些jar包也不会有什么问题。

  那么TestClass1是什么时候被AppClassLoader加载的呢?对于上述代码中的:

......
1.Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false);
2.System.out.println(clazz.getClassLoader());
3.System.out.println(TestClass1.class.getClassLoader());
......

  当代码执行到第三行打印TestClass1.class.getClassLoader的时候,会检查TestClass1.class是否已经被加载,如果没有加载则需要触发类加载的逻辑。这里需要注意的是,当前类(JavaMain)是被AppClassLoader加载的,它所依赖的类默认也会使用加载当它的类加载器(也就是AppClassLoader)去检查,这个叫做“全盘委派机制”(我也不知道官方是不是叫这个名字)。
  为了验证这一点,我们再新建一个TestClass2.java,在构造方法中打印类加载器:

public class TestClass2 {
    public TestClass2() {
        System.out.println("testClass2.classLoader:" + this.getClass().getClassLoader());
    }
}

  然后在TestClass1中创建一个方法触发TestClass2的实例化:

public class TestClass1 {
    public void run() {
        new TestClass2();
    }
}

  由于main方法中使用TestClass1会被AppClassLoader加载,所以我们不能强转类型,只能通过反射调用该方法:

public class JavaMain {
    public static void main(String[] args) throws Exception {
        String classPath = JavaMain.class.getClassLoader().getResource("").getPath();
        MyClassLoader2 myClassLoader = new MyClassLoader2(classPath, JavaMain.class.getClassLoader());
        Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false);
        Object obj = clazz.newInstance();
        Method method = obj.getClass().getDeclaredMethod("run", null);
        method.setAccessible(true);
        method.invoke(obj, null);
    }
}

  输出如下:

testClass2.classLoader:com.demo.classloader.MyClassLoader2@5cad8086

2.2 覆盖核心类?

  如果用户自定义一个全路径相同的Java核心类,能否有办法覆盖原版呢?正常情况下,根据双亲委派机制是没办法的:根据委派规则,加载动作会委派到BootStrapClassLoader,而BootStrap能加载这些核心类。既然如此,那么我们打破双亲委派尝试一下。
  首先在项目中创建一个java.util.HashMap:

package java.util;
public class HashMap {
}

  然后创建一个自定义类加载器,这个和之前类似:

public class MyClassLoader3 extends ClassLoader {
    private URLClassPath ucp;

    public MyClassLoader3(String path, ClassLoader parent) throws MalformedURLException {
        super(parent);
        this.ucp = new URLClassPath(new URL[]{new File(path).toURI().toURL()});
    }
    
	static {
        ClassLoader.registerAsParallelCapable();
    }
    
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Resource resource = ucp.getResource(name.replace('.', File.separatorChar).concat(".class"), false);
        if (resource != null) {
            try {
                byte[] bytes = resource.getBytes();
                Class clazz = defineClass(name, bytes, 0, bytes.length);
                if (resolve) {
                    resolveClass(clazz);
                }
                return clazz;
            } catch (IOException e) {
                throw new ClassNotFoundException(e.getMessage());
            }

        } else {
            throw new ClassNotFoundException();
        }
    }
}

  在main方法中创建自定义类加载器,加载路径为当前项目路径,然后尝试加载java.util.HashMap:

public static void main(String[] args) throws Exception {
        String classPath = JavaMain.class.getClassLoader().getResource("").getPath();
        MyClassLoader3 myClassLoader = new MyClassLoader3(classPath, JavaMain.class.getClassLoader());
        Class clazz = myClassLoader.loadClass("java.util.HashMap", false);
}

  当然不出意外的是,有异常堆栈抛出:

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.util
	at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:754)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
	at com.demo.classloader.MyClassLoader3.loadClass(MyClassLoader3.java:29)
	at com.demo.classloader.JavaMain.main(JavaMain.java:13)

  提示禁止加载包:java.util,看堆栈信息异常是ClassLoader.preDefineClass抛出来的,看看相应的代码:

private ProtectionDomain preDefineClass(String name, ProtectionDomain pd){
		......
        if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                ("Prohibited package name: " +
                 name.substring(0, name.lastIndexOf('.')));
        }
        ......
}

  源码写的很清楚,java.打头的包都不允许加载,所以我们项目中建包还是不要以java打头。
  既然检查工作是在preDefineClass中完成的,那么我们能否绕过predefineClass方法呢?
  现在回到类加载的流程,我们先通过findClass找到需要加载的字节码文件,这一步没有问题。找到字节码文件之后,需要调用defineClass方法生成Class,defineClass定义在ClassLoader中:

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;
    }

  该方法是一个final方法,我们无法重写,那能在自定义类加载器中调用defineClass1方法吗?defineClass1方法定义在ClassLoader中,是一个private native方法:

private native Class<?> defineClass0(String name, byte[] b, int off, int len,ProtectionDomain pd);
private native Class<?> defineClass1(String name, byte[] b, int off, int len,ProtectionDomain pd, String source);
private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,int off, int len, ProtectionDomain pd,String source);

  所以我们只能通过父类的defineClass创建Class,也就没法绕过preDefineClass方法的检查。既然如此,那能不能从本地方法入手呢?理论上是可行的,但是需要修改动态链接文件。但是都能操作dll了,还需要费尽心思去覆盖核心类库吗?

三、TCCL

  Thread Context ClassLoader(TCCL),即线程上下文类加载器。对于一些场景,可能会需要父类加载器调用子类加载器的情况,一个典型的例子就是SPI。
  对于某些功能,比如日志、JDBC等等,Java本身只提供接口,由用户自己实现或选择第三方提供的实现类,这样遵循了可插拔的特性。为了支持这点,Java提供了一种服务发现机制:为一些接口寻找具体的实现类。当作为服务提供者实现了某个服务接口之后,需要在jar包的META-INF/services/目录下创建一个以服务接口全限定名命名的文件,将接口实现类全限定名配置在该文件中。JDK提供了一个根据此规则寻找服务实现者的工具:ServiceLoader。使用ServiceLoader可以找到指定接口的实现类,进而完成服务实现者的加载。
  这其中出现的问题就是ServiceLoader是由启动类加载器加载,而服务实现者并不在其能加载的文件允许范围内,于是便出现了冲突。TCCL便能够解决这个问题,Thread类有一个contextClassLoader成员变量:

    /* The context ClassLoader for this thread */
    private ClassLoader contextClassLoader;

  通过相应的set方法:

Thread.currentThread().setContextClassLoader(classloader);

  将一个类加载器和线程绑定。这样在一个线程中,需要加载当前类加载器无法加载的类的时候,可以从当前线程中获取TCCL进行加载:

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

  TCCL默认为AppClassLoader,初次在sun.misc.Launcher的构造方法中设置:

public Launcher() {
        ......
        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
		//TCCL默认为AppClassLoader
        Thread.currentThread().setContextClassLoader(this.loader);
        ......
}

四、spring的类加载

  对于一个servlet容器来说,还是以Tomcat为例。一个webapps可以同时部署多个应用,而每个应用可能引用相同的jar包,在没有版本冲突的情况下,可以把这些jar包放到shared目录,由SharedClassLoader加载(不考虑高版本合并到lib目录),以达到让每个WebAppClassLoader共享的目的。
  对于每个webapp来说,其字节码文件默认由各自的WebAppClassLoader加载。但是像spring这种bean工厂来说,它要管理bean,就要能加载这些类,但是如果spring的jar包放在上层目录,其类加载器是无法加载webapp下的类的,该如何是好呢?
  其实这也是一个父类加载器需要反向调用的例子,使用TCCL就可以解决:spring在加载一个类的时候从当前线程获取TCCL,而servlet容器将TCCL设置为WebAppClassLoader。这样不论哪个webapp使用spring,spring使用的都是各自的WebAppClassLoader。就像这样:

ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
	//spring在webapp下,类加载器相同
	currentContext = this.context;
} else if (ccl != null) {
	//加载spring的类加载器和TCCL不同,将classLoader和WebApplicationContext用map
	//保存起来,用的时候根据classLoader获取context
	currentContextPerThread.put(ccl, this.context);
}

  当然,如果在SpringBoot中使用内嵌servlet容器的时候,就不会出现一个servlet容器包含多个应用的情况了,也就不用再用map维护不同的context了,直接使用TCCL即可:

		ClassLoader cl = null;
		try {
			cl = Thread.currentThread().getContextClassLoader();
		}
		catch (Throwable ex) {
			// Cannot access thread context ClassLoader - falling back...
		}
		if (cl == null) {
			// No thread context class loader -> use class loader of this class.
			cl = ClassUtils.class.getClassLoader();
			if (cl == null) {
				// getClassLoader() returning null indicates the bootstrap ClassLoader
				try {
					cl = ClassLoader.getSystemClassLoader();
				}
				catch (Throwable ex) {
					// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
				}
			}
		}
		return cl;

本文链接http://www.dzjqx.cn/news/show-617281.html