线程上下文类加载器与普通类加载器的区别

305

线程上下文类加载器和普通类加载器有什么区别?

也就是说,如果Thread.currentThread().getContextClassLoader()getClass().getClassLoader()返回不同的类加载器对象,哪一个会被使用?

4个回答

197
这并不是对原问题的回答,但由于该问题在任何涉及ContextClassLoader的查询中都排名很高并且被链接,我认为回答与之相关的问题是很重要的。简短的答案是:永远不要使用上下文类加载器!但当你必须调用缺少ClassLoader参数的方法时,请将其设置为getClass().getClassLoader()
当一个类的代码请求加载另一个类时,要使用的正确类加载器是调用者类的同一个类加载器(即getClass().getClassLoader())。这是99.9%的情况下的工作方式,因为这是JVM在第一次构造新类的实例、调用静态方法或访问静态字段时所做的
当你想要使用反射创建类(例如在反序列化或加载可配置的命名类时),进行反射的库应该总是询问应用程序要使用哪个类加载器,通过从应用程序接收ClassLoader作为参数来实现。应用程序(知道所有需要构造的类)应该传递getClass().getClassLoader()
任何其他获取类加载器的方式都是不正确的。如果库使用诸如Thread.getContextClassLoader()sun.misc.VM.latestUserDefinedLoader()或者sun.reflect.Reflection.getCallerClass()这样的黑科技,那就是API中存在缺陷造成的一个错误。简单来说,Thread.getContextClassLoader()之所以存在,只是因为设计ObjectInputStream API的人忘记接受ClassLoader作为参数,这个错误至今仍在困扰着Java社区。
也就是说,许多JDK类使用少数几种黑科技之一来猜测要使用的某个类加载器。有些使用ContextClassLoader(当您在共享线程池上运行不同的应用程序或将ContextClassLoader保留为空时会失败),有些遍历堆栈(当类的直接调用者本身是库时将失败),有些使用系统类加载器(这很好,只要它被记录为仅使用CLASSPATH中的类)或引导类加载器,有些则使用上述技术的不可预测组合(这只会使事情更加混乱)。这导致了许多人哭泣和咬牙切齿。
在使用此类API时,首先尝试查找接受类加载器作为参数的方法重载。如果没有明智的方法,则在调用API之前尝试设置ContextClassLoader(并在此后重置它)。
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
try {
    Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
    // call some API that uses reflection without taking ClassLoader param
} finally {
    Thread.currentThread().setContextClassLoader(originalClassLoader);
}

10
是的,这是我会向任何询问此问题的人指出的答案。 - Marko Topolnik
13
这个回答主要关注使用类加载器来加载类(通过反射或类似方式实例化它们),而它被用于的另一个目的(实际上,我个人所使用的唯一目的)是加载资源。同样的原则适用吗?还是有一些情况,你想通过上下文类加载器而不是调用者类加载器获取资源? - Egor Hans
3
请注意,getClass().getClassLoader()ThisClass.class.getClassLoader() 不一定相同,除非 ThisClassfinal 或者您知道子类不存在。请留意这一点。 - Jesse Glick
假设我在子线程中设置了原始类加载器,会出现什么问题吗?ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(originalClassLoader); // 调用一些使用反射而不带ClassLoader参数的API } - Shadab

173
每个类将使用其自己的类加载器来加载其他类。因此,如果 ClassA.class 引用了 ClassB.class,那么 ClassB 需要在 ClassA 的类加载器或其父类加载器的类路径中。
线程上下文类加载器是当前线程的类加载器。一个对象可以从 ClassLoaderC 中的一个类创建,并传递给由 ClassLoaderD 拥有的线程。在这种情况下,如果对象想要加载不在它自己的类加载器中的资源,则需要直接使用 Thread.currentThread().getContextClassLoader()

1
为什么你说ClassB必须在ClassA的加载器(或者ClassA的加载器的父级)的类路径上?难道ClassA的加载器不能重写loadClass(),这样即使ClassB不在它的类路径上,它也可以成功地加载ClassB吗? - Pacerier
11
确实,并非所有的类加载器都有类路径(classpath)。当我写“ClassB需要在ClassA的类加载器的类路径(classpath)上”时,我的意思是“ClassB需要可以被ClassA的类加载器加载”。90%的情况下它们的意思相同。但是,如果你没有使用基于URL的类加载器,则只有第二种情况是正确的。 - David Roussel
“ClassA.class 引用了 ClassB.class” 这句话是什么意思? - jameshfisher
2
当ClassA有一个导入语句引用ClassB,或者如果ClassA中有一个方法具有类型为ClassB的局部变量。这将触发ClassB的加载,如果它尚未被加载。 - David Roussel
我认为我的问题与这个主题有关。你对我的解决方案有什么看法?我知道这不是一个好的模式,但我没有其他想法如何修复它: http://stackoverflow.com/questions/29238493/org-jboss-mx-loading-loadmgr3-beginloadtask-after-invoke-clustered-jms-apps - Marcin Erbel

97
在 infoworld.com 上有一篇文章解释了 ClassLoader 的区别=> 应该使用哪种 ClassLoader (1)线程上下文类加载器提供了绕过类加载委托机制的后门。例如,JNDI 的核心实现是由 rt.jar 中的引导类实现的(从 J2SE 1.3 开始),但这些核心 JNDI 类可能会加载独立厂商实现的 JNDI 提供程序,并且可能在应用程序的-classpath 中部署。这种情况需要一个父类加载器(在这种情况下是原始类加载器)加载一个对其子类加载器(例如系统类加载器)可见的类。正常的 J2SE 委托无法工作,解决方法是使核心 JNDI 类使用线程上下文加载器,从而有效地“隧道”穿越类加载器层次结构,方向与适当的委托相反。 (2)同样来自该来源:这种混淆可能会在 Java 中持续一段时间。以任何具有任何类型动态资源加载的 J2SE API 为例,并尝试猜测它使用哪种加载策略。以下是抽样结果: - JNDI 使用上下文类加载器 - Class.getResource() 和 Class.forName() 使用当前类加载器 - JAXP 使用上下文类加载器(从 J2SE 1.4 开始) - java.util.ResourceBundle 使用调用者的当前类加载器 - Java 序列化 API 默认情况下使用调用者的当前类加载器 - 通过 java.protocol.handler.pkgs 系统属性指定的 URL 协议处理程序仅在引导程序和系统类加载器中查找

正如建议的那样,解决方法是使核心JNDI类使用线程上下文加载器,但我不明白这在这种情况下有何帮助。我们想要使用父类加载器加载实现供应商类,但它们对于父类加载器不可见。因此,即使我们将此父类加载器设置为线程的上下文类加载器,我们如何使用父类加载器来加载它们呢? - Sunny Gupta
6
@SAM,所提供的解决方法实际上与你在结尾所说的完全相反。不是将父级 bootstrap 类加载器设置为上下文类加载器,而是将子级 system 类路径类加载器设置为正在设置的 Thread。然后,JNDI 类确保使用 Thread.currentThread().getContextClassLoader() 来加载类路径上可用的 JNDI 实现类。 - Ravi K Thapliyal
普通的J2SE委托机制不起作用,我可以知道为什么吗?因为引导类加载器只能从rt.jar中加载类,而不能从应用程序的-classpath中加载类,对吗? - YuFeng Shen

45

在 @David Roussel 的回答中,需要补充的是,类可以由多个类加载器加载。

让我们了解一下 class loader 的工作原理。

引导类加载器 负责从 rt.jar 中加载标准 JDK 类文件,并且它是 Java 中所有类加载器的父级。引导类加载器没有任何父级。

扩展类加载器 将类加载请求委派给其父级引导类加载器,如果不成功,则从 jre/lib/ext 目录或 java.ext.dirs 系统属性指定的任何其他目录加载类。

系统或应用程序类加载器 负责从 CLASSPATH 环境变量、-classpath 或 -cp 命令行选项、JAR 内部 Manifest 文件中的 Class-Path 属性加载应用程序特定的类。

应用程序类加载器扩展类加载器 的子级,由 sun.misc.Launcher$AppClassLoader 类实现。

ClassLoader 遵循三个原则。

委派原则

Java中的类是在需要时加载的。假设您有一个名为Abc.class的应用程序特定类,首次加载此类的请求将发送到Application ClassLoader,它将委派给其父级Extension ClassLoader,后者进一步委派给Primordial或Bootstrap类加载器。
注意:除了大多数使用C等本地语言实现的Bootstrap类加载器外,所有Java类加载器都使用java.lang.ClassLoader实现。
可见性原则
根据可见性原则,子ClassLoader可以看到由父ClassLoader加载的类,但反之则不成立。
唯一性原则
根据此原则,父级加载的类不应再次由子ClassLoader加载。
来源:javin paul博客在javarevisited中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接