在明天的 OSGi 2012 社区活动上,我将以“如何使你的类库在不依赖 OSGi 的情况下进行友好地 OSGi”为主题进行演讲。在演讲中我将会提及 Java 的线程上下文类加载器(TCCL),但是整个演讲只有 25 分钟,我没有更多时间对此进行深入讨论。所以我写这篇博客希望能够帮助大家了解到一些相关背景信息。
本文中的很多技术信息和研究取自于 Peter Kriens 先生写的一篇没有公开的 OSGi 需求建议书。对此我已经获得了他的许可。

历史

线程上下文类装载器(TCCL)在 Java 体系中有一段非常有趣的历史。
Java 定义了一套类装载器的层次结构,在一个典型的 Java 运行时在底部的“application”装载器负责“classpath”(通过 -classpath/-cp 提供),而启动装载器负责的是 rt.jar 包里的一系列 JRE 类(译者注:启动装载器处于类装载器层次结构的最顶层,是所有类装载器的父装载器)。在中间还有一个名为“extension”的类装载器,它负责装载 JRE ext 目录中的类。类装载器实行双亲委派机制,也就是说应用类装载器可以看到启动类装载器所装载的所有的类,而启动类装载器却看不到应用类装载器所装载的类。
Sun 的这一机制最先是在远程方法调用(RMI)中实现 Java 序列化的时候碰到了问题。在从流中反序列化数据为 Java 对象时需要应用的类的相关信息,但是反序列化代码是由启动装载器所装载;根据双亲委派模型它也就访问不到那些类。这个问题的最终解决是通过给 JRE 添加了私有的本地代码,这些代码允许了对调用栈的检查,通过检查来找到第一个非 null 类的装载器。
但是,很多其他扩展包随后很快也遇到了同样的问题,而且他们不能够(也不应该!)都通过在 Sun JVM 中添加私有本地代码的办法来解决。大约在同一时间,J2EE 变得愈发重要。J2EE 是一个应用模型,其中的 Java 代码需要在一个严格约束的环境中才能运行。应用都运行在一个“仓库”中。每个应用运行在一个独立的类装载器中,所有线程是不能够跨应用/仓库执行的。应用当然可以使用由 VM 提供的扩展包以及由容器提供的那些包,但是应用之间还是无法使用来自对方的任何代码。此外,这一模型还遇到了由容器提供的包无法访问应用代码的问题。
为此 Java 1.2 引入了 TCCL:作为一个 tread-local 变量的和某个线程关联在一起的一个类装载器。这意味着什么?这意味着任何包都可以在任意时候通过当前调用线程访问“当前”上下文装载器。这一上下文装载器被期望予能够访问负责本次调用的特定应用类装载器,然后能够对应用的类进行访问。
Sun 自己也开始大量使用 TCCL,尽管对此还没有合适的规范。JDK 1.5 有 79 处引用到了 Thread.getContextClassLoader()。Java 1.4 之后,Java 在 JNDI、JAXP、CORBA、JMX、Xalan、Xerces、AWT、Beans、SQL、Logging、Prefs、RMI、Security、Swing 以及大部分的 XML 子系统的虚拟机实现上已经修改为使用上下文类装载器。此外大部分中间件库,比如 Hibernate、Saxon、Jakarta Commons Logging 等等也已开始使用 TCCL。Java 对 TCCL 的使用几乎没有提供任何规范或指南。这也就导致了几乎每个库都按照不同的策略对它的进行使用的后果。
关于正确使用 TCCL 的两个关键问题是:

  • 什么时候对它进行设定,由谁来设定?
  • 它应该能够访问哪些类?

基于 J2EE 的视角来看这些问题都比较容易回答,因为编程模型是受到约束的。容器掌控了所有的入口点(比如,它控制着执行 Servlet 的 HTTP 线程以及 EJB 的 RMI Socket 监听线程,禁止创建额外线程,等等),因此它能够保证 TCCL 总是在进入应用程序代码的入口处进行设置。由于应用程序是完全独立的,因此这些能够访问的类的集合也就仅仅是该应用所拥有的所有类了。
但是在一个类似于 OSGi 的运行时模块化的场景下这些问题就很难回答了。bundle 能够自由地创建它们自己的线程和切入点,并且也没有通用的办法来为一个“应用”确定为其提供类的 bundle 集合;的确,这里“应用”的概念本身就难以准确定义。我们可以限制编程模型并且要求一个应用应该作为一个没有任何依赖的独立的 bundle 进行部署,但这样做的话也就丢掉了使用 OSGi 的大部分好处。因此 OSGi 并没有试图去规定 TCCL 应该在什么时候使用。

TCCL 的替代品

在我们自己的 OSGi 规范的代码里,或者在任何显式 OSGi 支持的库里,我们无需担忧 TCCL,因为 OSGi 服务提供了一个远比任何基于类装载器途径来进行装载的途径更轻便的途径。但是(应用中)遗留的第三方库依旧是一个问题。很多这样的库试图在运行时根据名字去装载应用的类:比如,Hibernate 从 .hbm.xml 文件中读取类名然后为每个数据库记录创建这些类的实例。
当你将这样一个库放到任何模块化的场景下时 - 包括 OSGi、JBoss 模块或者 Jigsaw - 你会发现行不通,因为一个类仅通过类名不足以对其进行唯一标识。一个类的识别由其完整描述名以及定义它的类装载器构成(在 OSGi 中相当于包含它的 bundle)。因此作为类名的补充,我们还需要知道负责装载该类的类装载器。暨于由不同应用服务器创建的种类繁多的类装载环境,很多库试图使用一些探索式的方法来解决这个问题。TCCL 通常就是这样一个探索式解决办法之一,此外还有检查该库自己的类装载器,使用 JRE 扩展类装载器等等办法。
如果一个库只考虑 TCCL 的话会遇到一些阻碍:在我们该库之前就要显式地在我们的代码中设置 TCCL。幸运的是,这种情况很少发生,比如大多数这种库也将调用 Class.forName(),这意味着该库将会使用自己的类装载器来装载类。虽然这还远远不够理想,但是我们可以通过部署一个单独的片段来解决问题,而不是在我们的代码中散乱地调用 setContextClassLoader()。更好的做法当然是一个库完全避开类名并允许类对象的传递;或者至少提供一个 API 方法来设置类装载器以从中装载类名。
不幸的是很难事先对此进行预测 - 至少没有仔细检查(库)代码 - 该库采用了何种探索式解决办法,如上文所述这还是由于缺乏完整的规范和指南所造成的结果。

总结

我希望大家都能出席我明天的演讲,该演讲主要是针对想要自己的代码如何在 OSGi 中工作的更好的 Java 库的程序员们,但也不会仅局限于 OSGi。正如你从本文中能够推断出的那样,避免 TCCL 以及其他基于类装载器的怪异做法是实现友好 OSGi 的很重要的一点。此外我的演讲中还会涉及服务动态化以及一些配置问题。希望明天能看到你们!
2012 年 10 月 23 日
原文链接:The Dreaded Thread Context Class Loader

可怕的线程上下文类装载器(TCCL)的更多相关文章

  1. jvm(1)类的加载(三)(线程上下文加载器)

    简介: 类加载器从 JDK 1.0 就出现了,最初是为了满足 Java Applet 的需要而开发出来的. Java Applet 需要从远程下载 Java 类文件到浏览器中并执行. 现在类加载器在 ...

  2. 深入理解Java类加载器(二):线程上下文类加载器

    摘要: 博文<深入理解Java类加载器(一):Java类加载原理解析>提到的类加载器的双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器的实现方式.在Java ...

  3. JVM 线程上下文类加载器

    当前类加载器(Current ClassLoader) 每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指所依赖的类) 如果ClassX引用了ClassY,那么ClassX的类加载 ...

  4. 通过JDBC驱动加载深刻理解线程上下文类加载器机制

    关于线程上下文类加载器已经在之前学得比较透了,作为一个收尾,这里用平常J2EE开发时JDBC连接Mysql数据库常见的一段代码通过分析它的底层进一步加深对线程上下文类加载器的理解,所以先来将连接应用代 ...

  5. 【JVM学习笔记】线程上下文类加载器

    有许多地方能够看到线程上下文类加载的设置,比如在sun.misc.Launcher类的构造方法中,能够看到如下代码 先写一个例子建立感性认识 public class Test { public st ...

  6. cpu切换线程上下文会耗费多少时间

    cpu切换线程上下文会耗费多少时间,有人在linux下面使用不同的cpu测试过,需要1000ns以上的时间 https://blog.tsunanet.net/2010/11/how-long-doe ...

  7. 线程上下文类加载器ContextClassLoader内存泄漏隐患

    前提 今天(2020-01-18)在编写Netty相关代码的时候,从Netty源码中的ThreadDeathWatcher和GlobalEventExecutor追溯到两个和线程上下文类加载器Cont ...

  8. 7. 通过JDBC源码来分析线程上下文类加载器以及SPI的使用

    目录 1. 什么是全盘负责委托机制 2. 为什么需要有线程上下文类加载器 2.1 使用JDBC的例子,分析为什么双亲委托机制不能实现要求 2.2 线程上下文类加载器的作用 3. 线程上下文类加载器的使 ...

  9. 线程上下文类加载器(Context ClassLoader)

    1.线程上下文类加载器是从jdk1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader c1),分别用来 ...

随机推荐

  1. SSH项目整合

    其实框架的整合无非就是jar包和配置文件: struts2.spring.Hibernate这三个框架,分清楚什么作用就好配置了. jar包我们就不说了,这里看下配置文件吧: struts.xml: ...

  2. Chapter2(变量和基础类型)--C++Prime笔记

    数据类型选择的准则: ①当明确知晓数值不可能为负时,选用无符号类型. ②使用int执行整数运算.在实际应用中,short常常显得太小而long一般和int有一样的尺寸.如果运算范围超过int的表示范围 ...

  3. 五、java面向对象编程_3

    目录 十五.Object类 1.toString 2.equals 十六.对象转型(casting) 十七.动态绑定(多态) 十八.抽象类(abstract) 十九.final关键字 二十.接口 十五 ...

  4. [LeetCode] 398. Random Pick Index ☆☆☆

    Given an array of integers with possible duplicates, randomly output the index of a given target num ...

  5. C#_事件学习

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...

  6. Spring MVC处理响应的 header

    我们经常需要在HttpResponse中设置一些headers,我们使用Spring MVC框架的时候我们如何给Response设置Header呢? So easy, 看下面的代码: @Request ...

  7. zookeeper系列之:独立模式部署zookeeper服务

    一.简述 独立模式是部署zookeeper服务的三种模式中最简单和最基础的模式,只需一台机器即可,独立模式仅适用于学习,开发和生产都不建议使用独立模式.本文介绍以独立模式部署zookeeper服务器的 ...

  8. spark科普

    普Spark,Spark是什么,如何使用Spark(1)转自:http://www.aboutyun.com/thread-6849-1-1.html 阅读本文章可以带着下面问题:1.Spark基于什 ...

  9. Docker 初相见

    Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从Apache2.0协议开源. Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级.可移植的容器中,然后发布到任何流行的 Li ...

  10. Vim,Emacs排名不分先后

    关键词:Vim,Emacs,Vim和Emacs之争 一同时提到vim和emacs,就几乎一定引发关于哪个更好的圣战.据说这个圣战从很早就开始了,偶尔还会有windows下的ultraedit的用户来凑 ...