如何打破双亲委派

557 阅读5分钟

概述

我们都知道jvm加载类为了安全考虑,采用的是双亲委派机制,但是在一些场景下我们仍然向打破双亲委派。常见的场景有热部署,tomcat等。

双亲委派加载

双亲委派加载的流程简述:当classloader加载类时,先会查询是否已加载过,若加载过,直接返回;没有的话,就向父加载器传递,一直到bootstrap加载器,bootstrap判断是否可以加载,若可以的话,直接加载并返回,若不可以的话,则向下传递。详细可参考此文Class文件如何加载到JVM

先看下双亲委派的源码:

public abstract class ClassLoader {  
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //缓存中没查到,则请求父类尝试加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //最顶级父类bootstrapClassLoader加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                //父类中既没有缓存,也无法加载,则当前classloader尝试加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //自定义classLoader只能重写findClass方法
                    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;
        }
    }
}

loadClass是protected方法,那其实很简单,想要打破双亲委派机制的话,只要重写loadClass方法即可。

一般我们自定义类加载器,不需要打破双亲委派的话,只需要重写findClass方法

打破双亲委派

我们先写个demo来看看如何实现自定义类加载器

public class CustomFindClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return super.loadClass(name);
    }
​
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File file = new File("/home/yuhan/Documents/workspace/" + replace(name) + ".class");
        if (!file.exists()) {
            return super.findClass(name);
        }
        FileInputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;
        try {
            inputStream = new FileInputStream(file);
            outputStream = new ByteArrayOutputStream();
            int b = 0;
            while ((b = inputStream.read()) != 0) {
                outputStream.write(b);
            }
            byte[] bytes = outputStream.toByteArray();
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (Exception e) {
            }
        }
        return super.findClass(name);
    }
​
    private String replace(String  className) {
        return  className.replaceAll("\.", "/");
    }
​
    public static void main(String[] args) throws ClassNotFoundException {
        CustomFindClassLoader customFindClassLoader = new CustomFindClassLoader();
        Class first = customFindClassLoader.loadClass("com.roc.jvm.TestClass");
        System.out.println(first.getClassLoader());
​
        customFindClassLoader = new CustomFindClassLoader();
        Class second = customFindClassLoader.loadClass("com.roc.jvm.TestClass");
        System.out.println(second.getClassLoader());
        System.out.println(first == second);
​
    }
}

执行结果:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
true

我重写了findClass方法,然后尝试取加载类,通过结果发现,并不会使用自定义加载器加载,而是使用了AppClassLoader进行加载,并且加载的两个class是相同的。这就说明重写findClass方法是不会打破双亲委派的。

public class CustomLoadClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        File file = new File("/home/yuhan/Documents/workspace/" + replace(name) + ".class");
        if (!file.exists()) {
            return super.loadClass(name);
        }
        FileInputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;
        try {
            inputStream = new FileInputStream(file);
            outputStream = new ByteArrayOutputStream();
            int b = 0;
            while ((b = inputStream.read()) >= 0) {
                outputStream.write(b);
            }
            byte[] bytes = outputStream.toByteArray();
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (Exception e) {
            }
        }
        return super.loadClass(name);
    }
​
    private String replace(String  className) {
        return  className.replaceAll("\.", "/");
    }
​
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return super.findClass(name);
    }
​
    public static void main(String[] args) throws ClassNotFoundException {
        CustomLoadClassLoader customClassLoader = new CustomLoadClassLoader();
        Class first = customClassLoader.loadClass("com.roc.jvm.TestClass");
        System.out.println(first.getClassLoader());
​
        customClassLoader = new CustomLoadClassLoader();
        Class second = customClassLoader.loadClass("com.roc.jvm.TestClass");
        System.out.println(second.getClassLoader());
        System.out.println(first == second);
    }
}

执行结果:

com.roc.jvm.CustomLoadClassLoader@1218025c
com.roc.jvm.CustomLoadClassLoader@87aac27
false

重写了loadClass方法之后,就打破了双亲委派机制,使用了自定义加载器进行加载,而不是向上询问了,并且这是两个不同的class对象,同时这个例子也很好的解释了热部署的原理。

热部署

使用较新版本的idea会发现,settings里有个功能叫做hot swap,可以在项目启动后,对有改动的文件进行reload,就可以实现热部署功能,而无需重新启动。

在debug模式下,idea会使用两个加载器,一个用来加载例如第三方jar包等不变的类,另一个RestartClassLoader加载项目里的类。这样当某个java文件被修改之后,可以通过创建新的RestartClassLoader,将修改后的文件加载到内存中。

原理猜测

前提:类的唯一性由类加载器实例和类的全限定名一同确定

当修改后的类被新的类加载器加载后,肯定是两个不同的class对象,内存中class的引用指向了新加载的class对象

Tomcat

tomcat的类加载机制是打破双亲委派的,tomcat的类加载机制如下:

tomcat.png

(图片来源于网络)

JSP ClassLoader:用于jsp文件修改后的热重载

WebApp ClassLoader:各个webApp私有的类加载器,加载的class只对当前webApp可见

Shared ClassLoader:各个webApp共享的类加载,加载的class对所有webApp可见

Catalina ClassLoader:Tomcat容器私有的类加载器,对webApp不可见

Common ClassLoader:Tomcat最基础的类加载器,webApp和Tomcat均可见

原因:tomcat可以同时部署多个应用,每个应用可能存在相同的类库(使用Shared ClassLoader加载),同一类库的不同版本及各自的代码(WebApp ClassLoader),Tomcat本身所依赖的类库(Catalina ClassLoader),如果希望WebApp和Tomcat两者依赖的类库共享,那么就使用Common ClassLoader。

关于spi

JNDI ,JDBC 等都是 Java 的标准服务,基本都是由Bootstrap ClassLoader进行加载的。例如DriverManager.class,但是Driver是由第三方厂商根据JDBC的标准实现的,例如mysql,oracle等,Bootstrap ClassLoader是无法加载的,Jvm团队引入了Context ClassLoader进行加载,默认Context ClassLoader是AppClassLoader。

众所周知:核心类都由Bootstrap ClassLoader加载,而核心类的依赖类也应该由 Bootstrap ClassLoader加载,而JDBC这里DriverManager.class由Bootstrap ClassLoader加载,而其依赖类Driver.class因由第3方厂商实现,而无法由Bootstrap ClassLoader加载。在这里可以理解为是打破了双亲委派的。Driver.class实际是由App ClassLoader加载,而App ClassLoader加载class是符合双亲委派的

我们自定义加载器,是否可以重新加载String.class?

不可以,JVM设计者对于加载核心类设置了权限控制,核心类只能由Bootstrap ClassLoader加载。在加载时,判断如果以java开头,便会直接抛出异常

    private ProtectionDomain preDefineClass(String name,
                                            ProtectionDomain pd)
    {
        if (!checkName(name))
            throw new NoClassDefFoundError("IllegalName: " + name);
​
        // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
        // relies on the fact that spoofing is impossible if a class has a name
        // of the form "java.*"
        if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                ("Prohibited package name: " +
                 name.substring(0, name.lastIndexOf('.')));
        }
        if (pd == null) {
            pd = defaultDomain;
        }
​
        if (name != null) checkCerts(name, pd.getCodeSource());
​
        return pd;
    }