简述java类加载机制ClassLoader、双亲委派模型及类隔离加载

一、类加载器ClassLoader:

用于将Java类加载到Java虚拟机中,不同的类加载器加载的类不可能相等,每一个类,其唯一性都由加载他的类加载器和他本身一同确定,每一个类加载器,都有一个独立的类名称空间,换言之:即使两个类来源同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,这两个类就必定不相等(因此此时会在虚拟机中存在两个同名类,虽然来自一个Class文件,但依然各自独立)。其常用的类加载器有以下三种,当然除此之外还有用户自定义类加载器

  1. 根加载器(Bootstrap)
  2. 扩展加载器
  3. 应用程序类加载器

1.1 根加载器(BootstrapClassLoader):

C++编写,无法由java程序直接引用。JVM的核心加载器,随Java面世的第一版加载器。我们之所以安装好java,配置好环境变量,运行起java,就可以直接使用Object obj = new Object(),List list = new ArrayList()之类代码,是因为根加载器默认将${JAVA_HOME}/jre/lib/rt.jar加载到虚拟机中。(rt.jar为RunTime的缩写

1.2 扩展加载器(ExtensionClassLoader):

Java编写。由于Java诞生后不断的扩展优化,单独使用扩展类加载器加载${JAVA_HOME}/jre/lib/ext目录下的jar,库名通常以javax开头,例如swing等;扩展类库主要是为了兼容旧版本,但某些东西又有了新的解决方案,于是提供扩展类库。

1.3 应用程序类加载器(AppClassLoader):

面向程序员的加载器,会加载环境变量中CLASSPATH下的jar,自己生成的类或者第三方的类均有该加载器加载。

程序测试:

List list = new ArrayList();// java包
SwingNode swingNode = new SwingNode();// javax包
Student student = new Student();// 自定义java类

System.out.println(list.getClass().getClassLoader());
System.out.println(swingNode.getClass().getClassLoader());
System.out.println(student.getClass().getClassLoader());

运行结果: 【上述程序分别展示了三种类加载器对不同路径用途的class文件加载,因为根加载器使用C++语言编写,因此在java程序中只能打印出空值】

 可以看到三种java文件对应的类加载器,根加载器为null,扩展加载器及应用程序加载器都有一个sun.misc.Launcher前缀,实际上看下图路径,该Launcher类就是JVM的入口。

 通过代码体现父类继承关系:

List list = new ArrayList();
SwingNode swingNode = new SwingNode();
Student student = new Student();

System.out.println(swingNode.getClass().getClassLoader().getParent());
System.out.println(student.getClass().getClassLoader().getParent());
System.out.println(student.getClass().getClassLoader().getParent().getParent());

 运行结果:

类加载器加载顺序

二、双亲委派模型

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的父类加载器,如果父的类加载器没有加载,子类加载器才会尝试去加载。通俗的说就是:不论哪个class文件需要加载,首先给根加载器,若根加载器没找到,则交给扩展类加载器,如果还没找到,才会给应用程序类加载器,再找不到就会报ClassNotFound异常。

 

2.1 ClassLoader源码分析

ClassLoader为一个抽象类,在虚拟机启动后进行类加载会首先调用loadClassInternal方法,而该方法指向loadClass方法加载class文件,而双亲委派模型就是在该方法中实现。该方法首先判断该类是否已经被加载,若没被加载,则调用parent.loadClass方法加载(即父加载器),parent.loadClass方法中依然会判断parent是否为null,若依然有父类加载器,则继续递归向上调用,直至parent==null。当其父加载器抛出ClassNotFoundException时,说明没找到,则由子加载器继续加载,符合双亲委派机制。

public abstract class ClassLoader {

    private final ClassLoader parent;

    // 该方法在虚拟机启动进行类加载时会立刻调用
    private Class<?> loadClassInternal(String name)
        throws ClassNotFoundException
    {
        if (parallelLockMap == null) {
            synchronized (this) {
                 return loadClass(name);
            }
        } else {
            return loadClass(name);
        }
    }

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查请求的类是否已经被加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果父类加载器抛出ClassNotFoundException
                    // 说明父类加载器无法完成加载请求
                }

                if (c == null) {
                    // 在父类加载器无法加载时
                    // 再调用本身的findClass方法进行类加载
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}

好处

保证源代码不受污染。假设你自己定义了一个java.lang.String类,去运行,若没有此机制,会污染原本jdk中带的String类,导致代码出现不可预知的问题,程序将变得一片混乱。

2.2 破坏双亲委派模型

2.2.1 破坏历史

在Java诞生以来,历史上有三次大规模破坏:

第一次:双亲委派模型为JDK1.2推出,但在此之前就已经存在了ClassLoader,因此需要兼容。上述ClassLoader源码在loadClass方法中实现的双亲委派,因此如果想破坏最直接的办法就是继承ClassLoader并重写loadClass方法,该方法无疑是破坏性的,但因为JDK1.2以前都是这么做的,该接口无奈保留支持重写,但新的代码绝对不推荐如此使用。

ClassLoader在JDK1.2之后新增了一个findClass方法,目的就是当父类加载器加载失败时,将子类加载器的逻辑写在findClass中完成自定义设计。

第二次:模型自身缺陷导致。可见3.3,父加载器不能访问子加载器加载的类,因此当核心类库的Java需要调用自定义类的Java文件时,将加载失败。因此Java团队引入了线程上下文类加载器(Thread Context ClassLoader),该加载器可通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还没设置,则会从父线程中继承,如果父线程也没设置,那默认就是AppClassLoader。

第三次:代码热部署。我们总是希望修改了java类内容不重启JVM就可以看到他的改变,但默认的机制并不支持。想要支持需要引入第三方jar,该jar不再采用双亲委派模型。

2.2.2 什么时候需要打破

  • 假设在Tomcat类加载机制中,需要用一个技术来统一管理所有的项目,那就需要用CommonClassLoader来加载该技术代码,而此代码要对项目进行管理,就需要访问到具体的Web应用程序代码,此种情况违背了双亲委派原则,需要打破。
  • 典型的JNDI服务,例如:JDBC、JAXB等,其目的是对资源进行集中管理和查找,在JDK1.3时已经放入rt.jar,肯定是用Bootstrap ClassLoader来加载,但是其中的代码会调用自定义的类。例如JDBC连接MySQL,后续版本使用SPI加载机制,不需要手动设置驱动为com.mysql.jdbc.Driver,而是直接调用MySQL的jar中java.sql.Driver类,因为该类是第三方,Bootstrap ClassLoader看不到子类加载器的类,想要看到,必须打破双亲委派。

三、加载机制

3.1 预加载与延迟加载

预加载:JVM启动时,会预先加载一部分核心类库,包括:Object、String、Thread、Integer、Double、ClassLoader等核心到不能再核心的,由于其使用频率非常高,与其之后用到再加载,还不如启动JVM时直接加载内存以便使用,当然这些类不能过多,会降低JVM的启动速度。其中预加载ClassLoader可初始化扩展类加载器和应用程序类加载器。

延迟加载:除去上述预加载的java类,其余的无论是JDK类库还是其他类库的,都采用延迟加载。即使在程序中使用import引入了该类,只要不真正使用该类,JVM就不会进行加载,降低了系统开销。

3.2 缓存机制

所有Class被加载后,均会被缓存,每次需要时预先从缓存中寻找,若不存在,再加载该Class,因此每次修改了Java文件,都必须重启JVM,刷新缓存。

3.3 全盘负责

当某个类加载器负责加载Class时,该Class中依赖和引用的所有其他Class也将由该类加载器加载,除非显式的手动使用另一个类加载器强行加载,由2.1的源码分析可知,调用该类加载器的loadClass方法可加载范围为 自己+父类加载器,因此:

  • 一个由父类加载器加载的类若引入了子类加载器加载的类,则无法加载【父加载器不能访问子加载器加载的类】,例如:String类中使用了自定的Student类(当然此情况不可能发生)
  • 可正常加载一个由子类加载器加载的类中引入了父类加载器加载的类【子加载器可以访问父加载器加载的类】,例如:自定义的Student类中使用了String

3.4 类隔离、命名空间

ClassLoader相当于类的命名空间,起到了类隔离的作用。每一个类,其唯一性都由加载他的类加载器和他本身一同确定,每一个类加载器,都有一个独立的类名称空间,位于同一个ClassLoader里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader是类名称的容器,是类的沙箱。 

  • 父加载器无法访问子加载器加载的类
  • 子加载器可以访问父加载器加载的类
  • 相同级别的类加载器加载的类之间相互隔离

四、类加载器实用案例

4.1 Tomcat

作为一个主流的Java Web服务器,其应该具有以下几个特征:

  1. 部署在同一个服务器上的多个Web应用之间的Java类库应相互隔离。因为每个程序可能使用的同一个jar的版本不同,比如服务A使用了Spring1.0,服务B使用了Spring2.0;
  2. 部署在同一个服务器上的多个Web应用之间的Java类库可以共享;
  3. 服务器要保证自身服务不受Web服务影响,因此其自身的类库应与应用程序类库相互独立;
  4. 支持某些服务的热部署,比如JSP,因为其修改的概率远高于其他。

基于以上目的,Tomcat团队规划了类库结构和加载器:

在Tomcat5.x以前的版本,Tomcat目录有4个:

  • /common/*:该目录存放的类库可被Tomcat和所有Web应用程序共同使用
  • /server/*:该目录存放的类库仅可被Tomcat使用,对所有Web应用程序不可见
  • /shared/*:该目录存放的类库对Tomcat自己不可见,对所有Web应用程序可见
  • /WEB-INF/*:该目录存放的类库仅对此Web应用程序可见,对Tomcat和其他Web应用程序均不可见

为了支持这套目录结构,并对目录里的类库进行加载隔离(隔离技术请看3.4),Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,关系图如下:

  • CommonClassLoader:加载/common/*文件夹下的Java类库 
  • CatalinaClassLoader:加载/server/*文件夹下的Java类库
  • SharedClassLoader:加载/shared/*文件夹下的Java类库
  • WebAppClassLoader:加载/WEB-INF/*文件夹下的Java类库
  • JasperLoader:加载JSP文件

由于其层级关系:

  • CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用
  • CatalinaClassLoader和SharedClassLoader能加载的类与对方相互隔离
  • WebAppClassLoader可以使用SharedClassLoader加载到的类
  • WebAppClassLoader和JasperLoader通常有多个实例,每一个Web应用对应一个WebAppClassLoader实例,每一个JSP文件对应一个JasperLoader实例
  • 各个WebAppClassLoader和JasperLoader的实例之间相互隔离
  • JasperLoader的加载范围仅仅是这一个JSP文件翻译出来的Class文件,其出现的目的就是为了被丢弃,当服务器检测到JSP文件有修改,则建立一个新的JasperLoader替换掉当前的,来实现JSP文件的热加载。

在Tomcat6.x及以后版本,默认将/common、/server、/shared三个文件夹合并为/lib,这是Tomcat设计团队为简化大多数部署场景做的一项改进,若默认设置不能满足需求,可通过在tomcat/catalina/properties配置文件的server.loader和shared.loader配置项建立CatalinaClassLoader和SharedClassLoader实例,重新启用Tomcat5.x的加载器架构。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值