前言
在深入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[]); } 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[]); } @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[]); } 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 staticServiceLoaderload(Classservice) { 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;
原文地址:深入OpenJDK源码全面理解Java类加载器(下 -- Java源码篇)
本文链接地址:深入OpenJDK源码全面理解Java类加载器(下 -- Java源码篇),英雄不问来路,转载请注明出处,谢谢。
有话想说:那就赶紧去给我留言吧。
文章评论