关于JVM中双亲委派机制的讨论

技术 · 03-14 · 151 人浏览
关于JVM中双亲委派机制的讨论

双亲委派机制的概念

​ 双亲委派机制是指当一个类加载器收到某个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,它会先委托父类加载器在自己的搜索范围内找不到对应的类时,该类加载器才会尝试自己去加载。

双亲委派模式的工作流程

image-20240314165330815

双亲委派模型的核心代码

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 {
                    //返回由引导类加载器加载的类;如果未找到,则返回 null。
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果父类加载器抛出ClassNotFoundException异常
                // 则说明父类加载器无法完成加载请求
            }

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

                // 这是定义类加载器;记录统计数据
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

双亲委派机制的好处

  1. 避免类的重复加载
  2. 避免Java核心API被篡改

如何打破双亲委派机制?

(一) 为什么要打破双亲委派机制?

​ 有时我们需要多次加载同名目录下的类,比如:当我们在Tomcat上部署多个服务时,不同服务上可能依赖了不同版本的第三方jar,如果此时使用双亲委派机制加载类,会导致多个服务中第三方jar只加载一次,其他服务中的其他版本jar将不会生效,导致请求结果异常。为了避免这种情况,我们需要打破双亲委派机制,不再让父类[应用类加载器]加载,而是为每个服务创建自己的子类加载器。

(二) 如何打破双亲委派机制?

​ 打破双亲委派有两种方式:(1)不委派【SPI机制】;(2)向下委派。

​ Tomcat使用父类加载器加载了公用的jar,对于非公用的jar则使用自己的子类加载器进行单独加载。打破双亲委派需要重写findLoadedClass()方法。

image-20240314165911585

(三) 模拟Tomcat中WebappClassLoader打破双亲委派

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();
        }
    }
}

Java JVM
Theme Jasmine by Kent Liao