可怕的线程上下文类装载器(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),分别用来 ...
随机推荐
- 从function的定义看JavaScript的预加载
在JavaScript中定义一个函数,有两种写法: function ftn(){} // 第一种 var ftn = function(){} // 第二种 有人说,这两种写法是完全等价的.但是在解 ...
- Django JSON 时间
在views.py中导入: from django.core.serializers.json import DjangoJSONEncoder 在返回JSON数据时调用: return HttpRe ...
- Chapter6(函数) --C++Prime笔记
1.重载函数,也就是说一个名字可以对应几个不同的函数. 2.内置类型的未初始化局部变量将产生未定义的值. 3.局部静态对象在程序执行路径第一次进过对象定义语句时初始化,并且直到程序终止才被销毁. 内置 ...
- 我们使用git checkout 将B分支上的部分页面代码 添加或覆盖到A分支上
$ git branch * A B $ git checkout B message.html message.css message.js other.js $ git status # On b ...
- linux 中 virtualenvwrapper的使用
原文链接:http://www.jianshu.com/p/3abe52adfa2b Virtaulenvwrapper Virtaulenvwrapper是virtualenv的扩展包,用于更方便管 ...
- hadoop基础-SequenceFile详解
hadoop基础-SequenceFile详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.SequenceFile简介 1>.什么是SequenceFile 序列文件 ...
- table 样式美化
1. 单像素边框CSS表格 这是一个很常用的表格样式. 源代码: <!-- CSS goes in the document HEAD or added to your external sty ...
- centos7.2 rabbitmq3.6.2源码部署
1.安装所有依赖包yum install -y gcc ncurses ncurses-base ncurses-devel ncurses-libs ncurses-static ncurses-t ...
- Android 利用 gson 将 json 转成 对象object 集合list
1.build.gradle 中引入gson compile 'com.google.code.gson:gson:2.8.5' 2.将后台返回的json数据转对象.List.时间格式与后台返回的时间 ...
- openresty/1.11.2.1性能测试
测试数据 ab -n -c -k http://127.0.0.1/get_cache_value nginx.conf lua_shared_dict cache_ngx 128m; server ...