当JVM要加载某个类时,使用的是哪个ClassLoader?

这里先给出结论 : 当JVM要加载某个类时,是用加载当前类的ClassLoader加载的

用代码阐述就是 :
new ClassA()
等价于
this.getClass().getClassLoader.loadClass(“ClassA”).newInstance();

我们先看下面一段代码:

  1. public class TestClass {
  2. public static void main(String[] args) throws Exception {
  3. System.out.println("current classLoader:" + TestClass.class.getClassLoader());
  4. System.out.println("InitClazz's ClassLoader:" + InitClazz.class.getClassLoader());
  5. if(!(TestClass.class.getClassLoader() instanceof CustomClassLoader)){
  6. changeClassLoader();
  7. return;
  8. }
  9. System.out.println("finish");
  10. }
  11. private static void changeClassLoader() throws Exception {
  12. System.out.println("change to CustomClassLoader");
  13. CustomClassLoader classLoader = new CustomClassLoader("/Users/lvduo/develop/test/target/classes");
  14. Class testclazz = classLoader.loadClass("com.donar.classload.TestClass");
  15. Method main = testclazz.getMethod("main",String[].class);
  16. main.invoke(null, (Object) new String[]{});
  17. }
  18. }

首先Main函数运行,APPClassLoader加载TestClass.class的要一个Class对象ClassA,所以ClassA对应的类加载器应该是 APPClassLoader。InitClazz对应的APPClassLoader也是APPClassLoader。

接着changeClassLoader方法中重新用CustomClassLoader加载TestClass得到ClassB,然后通过反射调用ClassB的main方法,此时ClassB对应的累加载器应该是CustomClassLoader,故InitClazz对应的类加载器也是CustomClassLoader。

运行结果如下:

  1. current classLoader:sun.misc.Launcher$AppClassLoader@1b3e02ed
  2. InitClazz's ClassLoader:sun.misc.Launcher$AppClassLoader@1b3e02ed
  3. change to CustomClassLoader
  4. current classLoader:com.donar.classload.CustomClassLoader@7c2f1622
  5. InitClazz's ClassLoader:com.donar.classload.CustomClassLoader@7c2f1622
  6. finish

假如我们的App依赖了一个库A,版本是1.0,一般情况就是依赖了一个A-1.0.jar的jar包,然后App使用的中间件Dubbo也依赖了库A,但是依赖的是A-2.0.jar,同时App还使用了中间件MetaQ-Client,这个中间件依赖了A-3.0.jar。这种情况下,最终加载的库A是什么版本呢?如果我们使用maven来管理依赖,最终加载A的版本是不确定的(根据maven依赖冲突覆盖的顺序,一般以pom文件中先出现的A版本为准),这就给我们的应用带来了风险。利用上述方式将不同依赖模块的代码放在不同的类加载器下加载,只需要在入口处控制类加载器的切换就能实现多个版本的同名的class在同一个jvm共存,调用各个模块暴露的方法时使用的类的版本也不会错乱。阿里的Pandora就是这么个原理。

ContextClassLoader是做什么的?

ContextClassLoader是每个线程持有的一个ClassLoader的应用,当一个线程被创建时会从父线程继承。

典型场景的就是SPI机制,SPI是通过ServiceLoader去加载具体的实现类的,我们先看一段代码

  1. public class ThreadContextCL {
  2. public static void main(String[] args) {
  3. System.out.println("ServiceLoader's ClassLoader is " + ServiceLoader.class.getClassLoader());
  4. System.out.println("ThreadContextCL's ClassLoader is " + ThreadContextCL.class.getClassLoader());
  5. System.out.println("ContextClassLoader's ClassLoader is " + Thread.currentThread().getContextClassLoader());
  6. }
  7. }
  1. ServiceLoader's ClassLoader is null
  2. ThreadContextCL's ClassLoader is sun.misc.Launcher$AppClassLoader@4322394
  3. ContextClassLoader's ClassLoader is sun.misc.Launcher$AppClassLoader@4322394

从执行结果中看 ServiceLoader 是由 BootstrapClassLoader加载的。
根源还是在于ClassLoader双亲委托机制。根据此机制,父ClassLoader无法加载到子Classloader的classpath下的类。于是写ServiceLoader源码那哥们就在想我要加载具体的实现类,但是我有没能力加载,更可气的是我不确定哪个类加载器能加载,没办法了只好给自己留一个后门,弄了个线程上下文类加载器,ServiceLoader只需要使用用线程上下文加载器去加载这个类就行不用关心具体是哪个加载器,由负责调用的线程去设置已经加载具体实现类的ClassLoader到线程上下文就行。

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

Class.forName()发生了什么?

Class.forName()主要完成的就是类加载过程的前面两部,加载和链接
扒一下源码:

  1. public static Class<?> forName(String className)
  2. throws ClassNotFoundException {
  3. Class<?> caller = Reflection.getCallerClass();
  4. return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
  5. }

第一步.获取caller,这个caller就是调用这个方法的类。
第二步.获取caller的类加载器
第三部.调用本地方法,用caller的类加载器来加载传入className对应的类

所以Class.forName()也是完全符合我们针对第一个问题给出的结论的。

总结

类加载是java里比较好玩的一个部分,很多容器,框架都用利用了类加载的特性,比如tomcat就是利用双亲委派机制使得jar包中的类或共享或独立。tomcat的reload特性亦是如此。了解类加载相关知识后,我们能更好的了解它们的工作原理,特定的类加载场景,我们也能通过自定义类加载器来满足,类加载结合agent甚至可以改变java.lang包中的类的行为。最后,推荐两篇相关文章:

深入探索 Java 热部署
类的卸载机制