前几天想了一下,最近主要学习linux和httpd,所以tomcat源码阅读先放一放,可能到9月份左右再继续。不过先把已经写好的几篇陆续贴上来

tomcat用到很多ClassLoader相关的代码,如果缺乏这方面的背景知识,阅读源码会遇到很多障碍,所以本文首先总结一下这方面的内容,和tomcat源码的关系不大

1 标准的ClassLoader体系

1.1 bootstrap

bootstrap classloader是由JVM启动的,用于加载%JAVA_HOME%/jre/lib/下的JAVA平台自身的类(比如rt.jar中的类等)。这个classloader位于JAVA类加载器链的顶端,是用C/C++开发的,而且JAVA应用中没有任何途径可以获取到这个实例,它是JDK实现的一部分

1.2 extension

entension classloader用于加载%JAVA_HOME%/jre/lib/ext/下的类,它的实现类是sun.misc.Launcher$ExtClassLoader,是一个内部类

基本上,我们开发的JAVA应用都不太需要关注这个类

1.3 system

system classloader是jvm启动时,根据classpath参数创建的类加载器(如果没有显式指定classpath,则以当前目录作为classpath)。在普通的JAVA应用中,它是最重要的类加载器,因为我们写的所有类,通常都是由它加载的。这个类加载器的实现类是sun.misc.Launch$AppClassLoader

用ClassLoader.getSystemClassLoader(),可以得到这个类加载器

1.4 custom

一般情况下,对于普通的JAVA应用,ClassLoader体系就到system为止了。平时编程时,甚至都不会感受到classloader的存在

但是对于其他一些应用,比如web server,插件加载器等,就必须和ClassLoader打交道了。这时候默认的类加载器不能满足需求了(类隔离、运行时加载等需求),需要自定义类加载器,并挂载到ClassLoader链中(默认会挂载到system classloader下面)

2 双亲委派模型

从上面的图可以看到,classloader链,是一个自上而下的树形结构。一般来说,java中的类加载,是遵循双亲委派模型的,即:

当一个classloader要加载一个类时,首先会委托给它的parent classloader来加载,如果parent找不到,才会自己加载。如果最后也找不到,则会抛出熟悉的ClassNotFoundException

这个模型,是在最基础的抽象类ClassLoader里确定的:

[java] view plaincopy
  1. protected synchronized Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. // First, check if the class has already been loaded
  5. Class c = findLoadedClass(name);
  6. if (c == null) {
  7. try {
  8. if (parent != null) {
  9. c = parent.loadClass(name, false);
  10. } else {
  11. c = findBootstrapClassOrNull(name);
  12. }
  13. } catch (ClassNotFoundException e) {
  14. // ClassNotFoundException thrown if class not found
  15. // from the non-null parent class loader
  16. }
  17. if (c == null) {
  18. // If still not found, then invoke findClass in order
  19. // to find the class.
  20. c = findClass(name);
  21. }
  22. }
  23. if (resolve) {
  24. resolveClass(c);
  25. }
  26. return c;
  27. }

自定义ClassLoader的时候,一般来说,需要做的并不是覆盖loadClass()方法,这样的话就“破坏”了双亲委派模型;需要做的只是实现findClass()方法即可

不过,从上面的代码也可以看出,双亲委派模型只是一种“建议”,并没有强制保障的措施。如果自定义的ClassLoader无视此规定,直接自行加载,不将请求委托给parent,当然也是没问题的

在实际情况中,双亲委派模型被“破坏”也是很常见的。比如在tomcat里,webappx classloader就不会委托给上层的common classloader,而是先委托给system,然后自己加载,最后才委托给common;再比如说在OSGi里,更是有意完全打破了这个规则

当然,对于普通的JAVA应用开发来说,需要自定义classloader的场景本来就不多,需要去违反双亲委派模型的场景,更是少之又少

3 自定义ClassLoader

3.1 自定义ClassLoader的一般做法

从上面的代码可以看到,自定义ClassLoader很简单,只要继承抽象类ClassLoader,再实现findClass()方法就可以了

3.2 自定义ClassLoader的场景

事实上,需要实现新的ClassLoader的场景是很少的

注意:需要增加一个自定义ClassLoader的场景很多;但是,需要自己实现一个新的ClassLoader子类的场景不多。这是两回事,不可混淆

比如,即使在tomcat里,也没有自行实现新的ClassLoader子类,只是创建了URLClassLoader的实例,作为custom classloader

3.3 ClassLoader的子类

在JDK中已经提供了若干ClassLoader的子类,在需要的时候,可以直接创建实例并使用。其中最常用的是URLClassLoader,用于读取一个URL下的资源,从中加载Class

[java] view plaincopy
  1. @Deprecated
  2. public class StandardClassLoader
  3. extends URLClassLoader
  4. implements StandardClassLoaderMBean {
  5. public StandardClassLoader(URL repositories[]) {
  6. super(repositories);
  7. }
  8. public StandardClassLoader(URL repositories[], ClassLoader parent) {
  9. super(repositories, parent);
  10. }
  11. }

可以看到,tomcat就是在URLClassLoader的基础上,包装了StandardClassLoader,实际上并没有任何功能上的区别

3.4 设置parent

在抽象类ClassLoader中定义了一个parent字段,保存的是父加载器。但是这个字段是private的,并且没有setter方法

这就意味着只能在构造方法中,一次性地设置parent classloader。如果没有设置的话,则会默认将system classloader设置为parent,这也是在ClassLoader类中确定的:

[java] view plaincopy
  1. protected ClassLoader(ClassLoader parent) {
  2. this(checkCreateClassLoader(), parent);
  3. }
  4. protected ClassLoader() {
  5. this(checkCreateClassLoader(), getSystemClassLoader());
  6. }

4 ClassLoader隐性传递

“隐性传递”这个词是我乱造的,在网上和注释中没有找到合适的描述的词

试想这样一种场景:在应用中需要加载100个类,其中70个在classpath下,默认由system来加载,这部分不需要额外处理;另外30个类,由自定义classloader加载,比如在tomcat里:

[java] view plaincopy
  1. Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
  2. Object startupInstance = startupClass.newInstance();

如上,org.apache.catalina.startup.Catalina是由自定义类加载器加载的,需要额外编程来处理(如果是system加载的,直接new就可以了)

如果30个类,都要通过这种方式来加载,就太麻烦了。不过classloader有一个特性,就是“隐性传递”,即:

如果一个ClassA是由某个ClassLoader加载的,那么ClassA中依赖的需要加载的类,默认也会由同一个ClassLoader加载

这个机制是由JVM保证的,对于程序员来说是透明的

5 current classloader

5.1 定义

与前面说的extension、system等不同,在运行时并不存在一个实际的“current classloader”,只是一个抽象的概念。指的是一个类“当前的”类加载器。一个对象实例所属的Class,是由哪一个ClassLoader加载的,这个ClassLoader就是这个对象实例的current classloader

获得的方法是:

[java] view plaincopy
  1. this.getClass().getClassLoader();

5.2 实例

current classloader概念的意义,主要在于它会影响Class.forName()方法的表现,贴一段代码进行说明:

[java] view plaincopy
  1. public class Test {
  2. public void tryForName() {
  3. System.out.println("current classloader: "
  4. + this.getClass().getClassLoader());
  5. try {
  6. Class.forName("net.kyfxbl.test.cl.Target");
  7. System.out.println("load class success");
  8. } catch (ClassNotFoundException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. }

这个类调用了Class.forName()方法,试图加载net.kyfxbl.test.cl.Target类(Target是一个空类,作为加载目标,不重要)。这个类在运行时能否加载Target成功,取决于它的current classloader,能不能加载到Target

首先,将Test和Target打成jar包,放到classpath之外,jar包中内容如下:

然后在工程中删除Target类(classpath中加载不到Target了)

在Main中用system 加载Test,此时Test的current classloader是system,加载Target类失败

[java] view plaincopy
  1. public static void main(String[] args) {
  2. Test t = new Test();
  3. t.tryForName();
  4. }

然后,这次用自定义的classloader来加载

[java] view plaincopy
  1. public static void main(String[] args) throws Exception {
  2. ClassLoader cl = createClassLoader();
  3. Class<?> startupClass = cl.loadClass("net.kyfxbl.test.cl.Test");
  4. Object startupInstance = startupClass.newInstance();
  5. String methodName = "tryForName";
  6. Class<?>[] paramTypes = new Class[0];
  7. Object[] paramValues = new Object[0];
  8. Method method = startupInstance.getClass().getMethod(methodName,
  9. paramTypes);
  10. method.invoke(startupInstance, paramValues);
  11. }
  12. private static ClassLoader createClassLoader() throws MalformedURLException {
  13. String filePath = "c://hehe.jar";
  14. File file = new File(filePath);
  15. URL url = file.toURI().toURL();
  16. URL[] urls = new URL[] { url };
  17. ClassLoader myClassLoader = new URLClassLoader(urls);
  18. return myClassLoader;
  19. }

在想象中,这次Test的current classloader应该变成URLClassLoader,并且加载Target成功。但是还是失败了

这是因为前面说过的“双亲委派模型”,URLClassLoader的parent是system classloader,由于工程里的Test类没有删除,所以classpath里还是能找到Test类,所以Test类的current classloader依然是system classloader,和第一次一样

接下来把工程里的Test类也删除,这次就成功了

5.3 Class.forName()

前面说的是单个参数的forName()方法,默认使用current ClassLoader

除此之外,Class类还定义了3个参数的forName()方法,方法签名如下:

[java] view plaincopy
  1. public static Class<?> forName(String name, boolean initialize,
  2. ClassLoader loader)
  3. throws ClassNotFoundException

这个方法的最后一个参数,可以传递一个ClassLoader,会用这个ClassLoader进行加载。这个方法很重要

比如像JNDI,主体类是在JDK包里,由bootstrap加载。而SPI的实现类,则是由厂商提供,一般在classpath里。那么在JNDI的主体类里,要加载SPI的实现类,直接用Class.forName()方法肯定是不行的,这时候就要用到3个参数的Class.forName()方法了

6 ContextClassLoader

6.1 获取ClassLoader的API

前面说过,已经有2种方式可以获取到ClassLoader的引用

一种是ClassLoader.getSystemClassLoader(),获取的是system classloader

另一种是getClass().getClassLoader(),获取的是current classloader

这2种API都只能获取classloader,没有办法用来传递

6.2 传递ClassLoader

每一个thread,都有一个contextClassLoader,并且有getter和setter方法,用来在线程之间传递ClassLoader

有2条默认的规则:

首先,contextClassLoader默认是继承的,在父线程中创建子线程,那么子线程会继承父线程的contextClassLoader

其次,主线程,也就是执行main()方法的那个线程,默认的contextClassLoader是system classloader

6.3 例子

对上面例子中的Test和Main稍微改一下(Test和Target依然打到jar包里,然后从工程中删除,避免被system classloader加载到)

[java] view plaincopy
  1. public class Test {
  2. public void tryForName() {
  3. System.out.println("current classloader: "
  4. + getClass().getClassLoader());
  5. System.out.println("thread context classloader: "
  6. + Thread.currentThread().getContextClassLoader());
  7. try {
  8. Class.forName("net.kyfxbl.test.cl.Target");
  9. System.out.println("load class success");
  10. } catch (Exception exc) {
  11. exc.printStackTrace();
  12. }
  13. }
  14. }
[java] view plaincopy
  1. public static void main(String[] args) throws Exception {
  2. ClassLoader cl = createClassLoader();
  3. Class<?> startupClass = cl.loadClass("net.kyfxbl.test.cl.Test");
  4. final Object startupInstance = startupClass.newInstance();
  5. new Thread(new Runnable() {
  6. @Override
  7. public void run() {
  8. String methodName = "tryForName";
  9. Class<?>[] paramTypes = new Class[0];
  10. Object[] paramValues = new Object[0];
  11. try {
  12. Method method = startupInstance.getClass().getMethod(
  13. methodName, paramTypes);
  14. method.invoke(startupInstance, paramValues);
  15. } catch (Exception exc) {
  16. exc.printStackTrace();
  17. }
  18. }
  19. }).start();
  20. }

这次的tryForName()方法在一个子线程中被调用,并依次打印出current classloader和contextClassLoader,如图:

可以看到,子线程继承了父线程的contextClassLoader

同时可以注意到,contextClassLoader对Class.forName()方法没有影响,contextClassLoader只是起到在线程之间传递ClassLoader的作用

6.4 题外话

从这个例子还可以看出,一个方法在运行时的表现,在编译期是无法确定的

在运行时的表现,有时候取决于方法所在的类是被哪个ClassLoader加载;有时候取决于是运行在单线程环境下,还是多线程环境下

这在编译期是不可知的,所以在编程的时候,要考虑运行时的情况。比如所谓“线程安全”的类,并不是说它“一定”会运行在多线程环境下,而是说它“可以”运行在多线程环境下

7 总结

本文大致总结了ClassLoader的背景知识。掌握了背景,再阅读tomcat的源码,基本就不会遇到ClassLoader方面的困难

本文介绍了ClassLoader的标准体系、双亲委派模型、自定义ClassLoader的方法、以及current classloader和contextClassLoader的概念

其中最重要的是current classloader和contextClassLoader

用于获取ClassLoader的API主要有3种:

ClassLoader.getSystemClassLoader(); 
Class.getClassLoader(); 
Thread.getContextClassLoader();

第一个是静态方法,返回的永远是system classloader,也就是说,没什么用 
后面2个都是实例方法,一个是返回实例所属的类的ClassLoader;另一个返回当前线程的contextClassLoader,具体的结果都要在运行时才能确定

其中,contextClassLoader可以起到传递ClassLoader的作用,所以特别重要

ClassLoader背景知识相关推荐

  1. Tomcat(一):背景知识和安装tomcat

    1. 基础背景知识 1.1 java和jdk概念 无论是何种程序,要能在计算机上运行,必须能转换为二进制的机器语言才能和硬件进行交互,在机器语言的上层是汇编语言,再上层是C/C++这样较底层的语言,由 ...

  2. Visual Studio c++必要的背景知识--链接与编译

    HowTo: 1) 如何创建和编写静态链接库 2) 如何创建可执行文件 3) 如何创建头文件和cpp文件 4) 如何相对路径寻址 5) 如何进行手动静态库链接 6) 如何编译部分或全部程序 在撰写Li ...

  3. mysql象限和投影_PostGIS空间数据库SRID背景知识 - 地理坐标系(球面坐标系)和投影坐标系(平面坐标系) - GIS开发者...

    背景 背景知识和坐标系有关. 什么是地理坐标系,什么是投影坐标系? 参考此文: 原文 1.首先理解地理坐标系(Geographic coordinate system),Geographic coor ...

  4. JAVA学习笔记--4.多线程编程 part1.背景知识和内存模型

    2019独角兽企业重金招聘Python工程师标准>>> 背景知识 CPU Cache 如上简易图所示,在当前主流的CPU中,每个CPU有多个核组成的.每个核拥有自己的寄存器,L1,L ...

  5. 用Python爬网页需要了解什么背景知识

    在知乎上有一位同学提出的问题:用Python爬网页需要了解什么背景知识,恰好我对爬虫有所了解,所以昨天晚上做了回答,今天放到公众号上面希望对大家有所帮助,如有帮助欢迎转发. 文中涉及到一些教程链接在本 ...

  6. 抽取+生成:一种基于背景知识的参考感知网络对话模型

    「论文访谈间」是由 PaperWeekly 和中国中文信息学会社会媒体处理专委会(SMP)联合发起的论文报道栏目,旨在让国内优质论文得到更多关注和认可. 目前,对话系统(Dialogue System ...

  7. MySQL高级 - 锁 - InnoDB行锁 - 介绍及背景知识

    行锁介绍 行锁特点 :偏向InnoDB 存储引擎,开销大,加锁慢:会出现死锁:锁定粒度最小,发生锁冲突的概率最低,并发度也最高. InnoDB 与 MyISAM 的最大不同有两点:一是支持事务:二是 ...

  8. 深度残差收缩网络:(一)背景知识

    总共六篇文章: 深度残差收缩网络:(一)背景知识 深度残差收缩网络:(一)背景知识_马鹏森的博客-CSDN博客 深度残差收缩网络:(二)整体思路 深度残差收缩网络:(二)整体思路_马鹏森的博客-CSD ...

  9. ×××背景知识技术介绍

    ×××背景知识技术介绍 <?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" / ...

最新文章

  1. 小插件 打开Android程序动画,android-单击小部件后如何启动活动?
  2. python奖励多少钱_关于python的问题,好的高奖励!
  3. cmos存储器中存放了_CMOS存储器中存放了计算机的一些参数和信息,其中不包含在内的是( )。_学小易找答案...
  4. JAX-RS 2.x与Spring MVC:返回对象列表的XML表示
  5. 四大开源项目联合发布 腾讯已成Github全球贡献前十公司!
  6. 联发科(MediaTek)Pentonic 电视芯片将率先支持杜比视界 IQ 精准细节功能
  7. Bootcamp Mac 安装Win10 教程
  8. Android用户界面 UI组件--AdapterView及其子类(一) ListView及各种Adapter详解
  9. (转)比特币核心钱包(Bitcoin Core)入门使用教程
  10. 数据分析工作常见的七种错误及规避技巧
  11. 小甲鱼c语言 23课指针 数组和数组指针
  12. APP部分漏洞及解决方法
  13. RC / RL并联电路计算
  14. 计算机硕士研究生毕设选题方向推荐 - 题目推荐
  15. 最新个人所得税计算方法
  16. 7_Arya_superbeyone_新浪博客
  17. CentOS 7 安装搜狗拼音输入法
  18. Fiddler使用介绍
  19. Hadoop学习与使用
  20. 二代测序之SNV基础知识笔记总结

热门文章

  1. python编程(反汇编)
  2. 华东理工大学和暨南大学计算机考研,2016华东理工大学VS暨南大学 孰强孰弱?...
  3. delete响应服务器,rest-RESTful-DELETE响应主体应包含什么
  4. c语言基础编程题讲解,C语言入门例题讲解
  5. mysql 按周分组_如何在MySQL中按周分组?
  6. postman测试登录后的接口_中文版Postman测试需要登陆才能访问的接口(基于Cookie)...
  7. 开始位置 环状图_【技术分享】如何找到压铸模具中真空阀的最佳位置?
  8. SimpleDateFormat的parse与format的用法区别
  9. android版本英文,Android API Level与sdk版本中英文对照表
  10. x12arima季节调整方法_你所不知道的秋冬季节养生要点,都在这里!