JVM系列(三):打破双亲委派及案例

本文探讨了打破Java双亲委派机制的原因及方法,包括自定义类加载器和Tomcat的具体实现。介绍了如何通过自定义类加载器重写loadClass方法来实现,以及Tomcat如何解决类库版本冲突和热加载等问题。

打破双亲委派机制

上一章我们讲到了类加载器双亲委派机制的一些原理,对于双亲委派机制,我们也了解了双亲委派机制有沙箱安全机制避免类的重复加载两大优点,这一章我们来讲述为什么要打破双亲委派机制以及如何打破双亲委派机制。并通过一些案例详细讲述打破双亲委派。

双亲委派机制

关于双亲委派机制,上一章有详细解释,其原理总结成一句话就是是:先委托给父亲加载,不行再派发给儿子自己加载。而对于双亲委派机制来说,有以下两大好处:

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

基于以上两大好处,可以看出双亲委派机制在安全高效方面卓有成效,但是大家也知道TomcatSPI机制等都有打破双亲委派的操作,于是就有了以下疑问:

为什么需要打破双亲委派?

  1. 同一个JVM中有需要支持同一第三方类库的不同版本同时运行。
    • 如Tomcat,对于不同的war包,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库以及Tomcat本身引用类库都是独立的,保证相互隔离
  2. JVM不重启情况下实现class文件的热加载
    • 如Tomcat,对于jsp文件要在JVM不重启的情况下实现热加载。而如果按照传统双亲委派机制加载,jsp文件修改后,类加载器会直接取方法区中已经存在的,并不会重新加载。所以可以通过为每一个class文件单独创建一个ClassLoader,每次更新class文件后,卸载之前的ClassLoader,重新加载。

打破双亲委派

从上文可以知道,双亲委派机制虽然在安全和高效方面卓有成效,但是在一些特殊的场景下,我们不得不采取一些措施打破双亲委派机制,以下为一些打破双亲委派的案例

自定义类加载器打破双亲委派机制

对于双亲委派机制,我在上一篇文章《Java类加载器和双亲委派机制详解》中有提到有关双亲委派机制的代码在ClassLoader中的loadClass方法中,所以只需要继承ClassLoader重写loadClass方法修改双亲委派部分源码,就能打破双亲委派机制。

public class MyClassLoader extends ClassLoader {
    private final String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    /**
     * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
     *
     * @param name    类的二进制名称
     * @param resolve 是否需要解决该类,一般为false
     * @return 二进制名称(binary name)对应的Class对象
     * @throws ClassNotFoundException 如果类未被找到,会抛出ClassNotFoundException
     */
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        // 基于类的binary name,对相同名称的类加锁,防止多个线程重复加载。这点不变
        synchronized (getClassLoadingLock(name)) {
            // 首先,先检查类是否已被加载,避免重复加载。这点不变
            Class<?> c = findLoadedClass(name);
            // 如果没找到,通过findClass加载。这点不变
            if (c == null) {
                long t1 = System.nanoTime();
                /*
                 * 重点
                 * 该处删除了委托parent(父加载器)加载的过程
                 * 直接通过自定义findClass方法加载
                 * 
                 */
                c = findClass(name);
                // ------------- 以下为JDK8原逻辑,删除部分时间计算逻辑 --------------
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    private byte[] loadByte(String name) throws Exception {
        // 替换为实际地址
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = null;
        byte[] data = null;
        try {
            // 加载class文件
            fis = new FileInputStream(classPath + "/" + name + ".class");
            int len = fis.available();
            data = new byte[len];
            fis.read(data);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                fis.close();
            }
        }
        return data;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 加载class文件
            byte[] data = loadByte(name);
            //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
}

Tomcat打破双亲委派机制

首先,我们来考虑:

作为一个web容器,Tomcat要解决什么问题:

  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。

  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。

  3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。

  4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改能够热加载

对于这些问题,我们不禁有了疑问:

Tomcat 如果使用默认的双亲委派类加载机制行不行?

  • 问题1,如果同一个第三方类库的不同版本,意味着类名和加载路径也大概率相同,默认加载器对于同一类只会加载一次

  • 问题2和问题3,默认加载器可以实现,因为他的职责就是保证唯一性

  • 问题4,如果使用默认加载器,类加载器会直接取方法区中已经存在的,并不会重新加载。这时候就要考虑为每一个jsp文件单独创建一个ClassLoader,每次更新jsp文件后,卸载之前的ClassLoader,重新加载。

Tomcat具体实现

Tomcat的ClassLoader结构:

请添加图片描述

可以看到CommonClassLoader、CatalinaClassLoader、SharedClassLoader是Tomcat自己定义的类加载器,它们分别加载/common/*/server/*/shared/*(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。其中WebAppClassLoader和JasperLoader类加载器通常会存在多个实例,每一个Web应用程序对应一个WebAppClassLoader,每一个JSP文件对应一个JasperLoader。

  • CommonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • CatalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • SharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本;

从图中的委派关系中可以看出:

  • CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。

  • WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离

  • 而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.class文件,它出现的目的就是为了被丢弃:当Web容器检测到jsp文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。

tomcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?

答案是:违背了。

很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制

模拟Tomcat中WebappClassLoader打破双亲委派

对于WebappClassLoader,我们知道,不同的war包有不同的WebappClassLoader加载不同版本的依赖,也有部分需要用到SharedLoader相关依赖。我们就模拟这一过程。

public class MyClassLoader extends ClassLoader {
    private final String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    /**
     * 重写类加载方法,模拟Tomcat打破双亲委派过程。
     * 对特定类自己加载,其他类还是通过父加载器加载
     *
     * @param name    类的二进制名称
     * @param resolve 是否需要解决该类,一般为false
     * @return 二进制名称(binary name)对应的Class对象
     * @throws ClassNotFoundException 如果类未被找到,会抛出ClassNotFoundException
     */
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        // 基于类的binary name,对相同名称的类加锁,防止多个线程重复加载。这点不变
        synchronized (getClassLoadingLock(name)) {
            // 首先,先检查类是否已被加载,避免重复加载。这点不变
            Class<?> c = findLoadedClass(name);
            // 如果没找到,通过findClass加载。这点不变
            if (c == null) {
                long t1 = System.nanoTime();
                /*
                 * 重点
                 * 该处对于com.tomcat.webapp(只是模拟)包下的class自己加载
                 * 对于其他class文件还是委托父类加载
                 */
                if (!name.startsWith("com.tomcat.webapp")){
                    c = this.getParent().loadClass(name);
                }else{
                    c = findClass(name);
                }
                // ------------- 以下为JDK8原逻辑,删除获取父加载器时间 --------------
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    private byte[] loadByte(String name) throws Exception {
        // 替换为实际地址
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = null;
        byte[] data = null;
        try {
            // 加载class文件
            fis = new FileInputStream(classPath + "/" + name + ".class");
            int len = fis.available();
            data = new byte[len];
            fis.read(data);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                fis.close();
            }
        }
        return data;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 加载class文件
            byte[] data = loadByte(name);
            //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
}
<think>嗯,用户问的是如何打破JVM双亲委派模型。首先,我需要回忆一下双亲委派模型的基本概念。双亲委派模型是Java类加载器的工作机制,类加载器在加载类时会先委托父类加载器去尝试加载,只有父类加载器无法完成加载时,子类加载器才会自己加载。这个机制的好处是保证了Java核心库的类型安全,避免重复加载。 接下来,用户想知道如何打破这个模型。我记得在Java中,类加载器的loadClass方法默认实现了双亲委派,如果要打破,通常需要重写这个方法,改变其加载逻辑。比如,先自己尝试加载类,而不是先委托给父类加载器。这可能涉及到自定义类加载器的编写。 然后,我需要考虑具体的实现方法。比如,在自定义类加载器中覆盖loadClass方法,改变委托顺序。或者使用线程上下文类加载器(TCL),在某些场景下,比如JNDI服务,会使用TCL来加载资源,这样可以绕过双亲委派。还有OSGi框架的例子,它通过网状结构的类加载器来实现模块化,每个Bundle有自己的类加载器,相互之间可以委托,这显然打破双亲委派模型。 不过,用户可能更关注具体的步骤和代码示例。需要举出实际的例子,比如如何重写findClass和loadClass方法,或者如何利用TCL来加载类。同时,应该提到打破双亲委派的应用场景,比如热部署、模块化加载等,这样用户能理解为什么需要这样做。 另外,需要注意打破双亲委派可能带来的问题,比如类冲突、安全性问题等,需要提醒用户谨慎使用。可能用户是遇到了特定的场景,比如动态加载不同的类版本,或者是开发应用服务器需要隔离不同应用的类,这时候打破双亲委派是必要的。 最后,总结一下实现方式,并强调适用场景和潜在风险。确保回答结构清晰,步骤明确,有代码示例更好,这样用户能更好地理解和应用。</think>关于打破双亲委派模型的问题,我们可以从实现原理、典型场景到具体实现分个层次来解析: --- ### **一、类加载机制基础** 标准双亲委派流程(JDK9+模块化后有所调整): $$ \begin{cases} 1. \text{检查是否已加载} \rightarrow \text{已加载则直接返回} \\ 2. \text{递归调用父加载器的} \ loadClass() \\ 3. \text{父类无法加载时,调用} \ findClass() \ \text{方法} \end{cases} $$ --- ### **二、打破方式及实现原理** #### **1. 线程上下文类加载器(TCL)** ```java // 典型场景:JDBC驱动加载 ClassLoader original = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(new CustomClassLoader()); // SPI服务加载(如java.sql.DriverManager) ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class); } finally { Thread.currentThread().setContextClassLoader(original); } ``` * **实现原理**:通过反向委派机制,由核心类库(如rt.jar)调用TCL加载实现类 #### **2. 重写loadClass()方法** ```java public class BreakDelegateLoader extends ClassLoader { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { // 优先自行加载指定包下的类 if (name.startsWith("com.myapp.hotswap")) { return findClass(name); } return super.loadClass(name); // 其他类仍走双亲委派 } @Override protected Class<?> findClass(String name) { // 从特定路径加载类字节码 byte[] classData = loadClassData(name); return defineClass(name, classData, 0, classData.length); } } ``` * **关键点**:破坏委派顺序,先尝试自己加载再委派给父类 #### **3. OSGi类加载模型** ``` ┌───────────┐ │ Bundle │ │ ClassLoader◄───┐ └─────┬─────┘ │ │ │ 动态委派 ┌─────▼─────┐ │ │ Bundle │ │ │ ClassLoader├───┘ └───────────┘ ``` * **网状结构**:每个Bundle有独立类加载器,通过Import-Package/Export-Package声明依赖关系 --- ### **、典型应用场景** | 场景 | 实现方式 | 代表案例 | |---------------------|-------------------------|--------------------------| | 热部署 | 自定义类加载器 | Tomcat Webapp隔离 | | SPI服务加载 | 上下文类加载器 | JDBC驱动加载 | | 模块化动态加载 | 网状委派模型 | OSGi/Equinox框架 | | 版本隔离 | 层级覆盖加载 | Maven依赖冲突解决方案 | --- ### **四、注意事项** 1. **类冲突风险**:当不同加载器加载同名类时,JVM会判定为不同类 $$ \text{Class}_A \neq \text{Class}_B \iff \text{ClassLoader}_A \neq \text{ClassLoader}_B $$ 2. **资源泄漏**:自定义类加载器需注意卸载机制,避免PermGen/Metaspace溢出 3. **安全性**:覆盖核心类时需配置安全策略文件 ```java // 设置安全管理器 System.setSecurityManager(new SecurityManager()); ``` 在实际开发中,建议优先遵循双亲委派模型,仅在确实需要类隔离、热加载等场景时谨慎使用破坏性方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值