可怕的线程上下文类装载器(TCCL)
在明天的 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)的更多相关文章
- jvm(1)类的加载(三)(线程上下文加载器)
简介: 类加载器从 JDK 1.0 就出现了,最初是为了满足 Java Applet 的需要而开发出来的. Java Applet 需要从远程下载 Java 类文件到浏览器中并执行. 现在类加载器在 ...
- 深入理解Java类加载器(二):线程上下文类加载器
摘要: 博文<深入理解Java类加载器(一):Java类加载原理解析>提到的类加载器的双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器的实现方式.在Java ...
- JVM 线程上下文类加载器
当前类加载器(Current ClassLoader) 每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指所依赖的类) 如果ClassX引用了ClassY,那么ClassX的类加载 ...
- 通过JDBC驱动加载深刻理解线程上下文类加载器机制
关于线程上下文类加载器已经在之前学得比较透了,作为一个收尾,这里用平常J2EE开发时JDBC连接Mysql数据库常见的一段代码通过分析它的底层进一步加深对线程上下文类加载器的理解,所以先来将连接应用代 ...
- 【JVM学习笔记】线程上下文类加载器
有许多地方能够看到线程上下文类加载的设置,比如在sun.misc.Launcher类的构造方法中,能够看到如下代码 先写一个例子建立感性认识 public class Test { public st ...
- cpu切换线程上下文会耗费多少时间
cpu切换线程上下文会耗费多少时间,有人在linux下面使用不同的cpu测试过,需要1000ns以上的时间 https://blog.tsunanet.net/2010/11/how-long-doe ...
- 线程上下文类加载器ContextClassLoader内存泄漏隐患
前提 今天(2020-01-18)在编写Netty相关代码的时候,从Netty源码中的ThreadDeathWatcher和GlobalEventExecutor追溯到两个和线程上下文类加载器Cont ...
- 7. 通过JDBC源码来分析线程上下文类加载器以及SPI的使用
目录 1. 什么是全盘负责委托机制 2. 为什么需要有线程上下文类加载器 2.1 使用JDBC的例子,分析为什么双亲委托机制不能实现要求 2.2 线程上下文类加载器的作用 3. 线程上下文类加载器的使 ...
- 线程上下文类加载器(Context ClassLoader)
1.线程上下文类加载器是从jdk1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader c1),分别用来 ...
随机推荐
- P1850 换教室
P1850 换教室 现在有一张图, 有 \(v <= 300\) 个节点 你需要从 \(c_{1}\) 到 \(c_{2}\) 到 \(c_{n} (n <= 2000)\) 现在你有 \ ...
- Hadoop生态圈-hbase介绍-完全分布式搭建
Hadoop生态圈-hbase介绍-完全分布式搭建 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.
- Hi3518 网络监控SOC芯片规格参数
Hi3518 网络监控SOC芯片 视频编解码 处理器内核 ● ARM926@ 440MHz,16KB I-Cache ,16KB D-Cache 视频编码 ● H.264 Main Pro ...
- bzoj千题计划163:bzoj1060: [ZJOI2007]时态同步
http://www.lydsy.com/JudgeOnline/problem.php?id=1060 以激发器所在节点为根 终止节点一定是叶节点 记录点的子树内最深的终止节点 然后从根往下使用道具 ...
- stl空间配置器简介
1. 符合STL标准的空间配器接口 STL是c++中使用非常广泛的一个标准库,它包含各种有用的容器.而空间配置器作为STL各种容器的背后的核心,负责容器内部内存的分配和释放.不过空间配置器可以分配的也 ...
- 一个highcharts混合图Demo
公司要我做一个highcharts的mockup,其实半个小时就能做完了,我做了将近两个小时,唉!不过还好,总算把东西学会了.勤能补拙! 把代码贴上来 布局很简单,一个div里套两个div,给好id, ...
- HDU 2594 KMP
题目链接 题意:给定两个字符串s1,s2,求最长的s1前缀s使得s为s2的最长后缀,输出该字符串和其长度. 题解:调换s1和s2的顺序,用KMP求解即可. #include <bits/stdc ...
- Oracle嵌套表
一.介绍 1.定义 嵌套表是表中之表.一个嵌套表是某些行的集合,它在主表中表示为其中的一列.对主表中的每一条记录,嵌套表可以包含多个行.在某种意义上,它是在一个表中存储一对多关系的一种方法. ...
- OpenCV LIBTIFF_4.0 link errors
以前用Caffe用的好好的,今天重装后居然报了很多这样的错误 /usr/lib/libopencv_highgui.so.' 1> /usr/lib/libopencv_highgui.so.' ...
- UNIX环境高级编程 第14章 高级I/O
这一章涉及很多概念和函数,包括:非阻塞I/O.记录锁.I/O复用.异步I/O.readv和writev函数以及内存映射. 非阻塞I/O 在Unix中,可以将系统调用分为两种,一种是“低速”系统调用,另 ...