美文网首页程序员
这篇关于JVM类加载器和双亲委派机制的笔记写的太好了,建议收藏起

这篇关于JVM类加载器和双亲委派机制的笔记写的太好了,建议收藏起

作者: 程序员伟杰 | 来源:发表于2020-08-11 14:08 被阅读0次

    前言

    Java里有如下几种类加载器

    1. 启动类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库比如 rt.jar、charsets.jar等。
    2. 扩展类加载器(ExtClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包。
    3. 应用程序类加载器(AppClassLoader):负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类。
    4. 自定义加载器:负责加载用户自定义路径下的类包。

    通过以下实例来了解各个类加载器:

    public class ClassLoaderTest {
    
        public static void main(String[] args) {
            System.out.println(Object.class.getClassLoader());
            // java提供的与DNS服务交互的api
            System.out.println(DNSNameService.class.getClassLoader());
            System.out.println(ClassLoaderTest.class.getClassLoader());
    
        }
    }
    
    

    运行结果如下:

    null
    sun.misc.Launcher$ExtClassLoader@6d6f6e28
    sun.misc.Launcher$AppClassLoader@58644d46
    
    

    启动类加载器是在有jvm底层创建的实例,所以在获取时为null,Object类是有启动类加载器进行加载的,所以获取其加载器时为null,而DNSNameService为JAVA_HOME/jre/lib目录下ext文件夹在的dnsns.jar包中的类,由扩展类加载器(ExtClassLoader)加载。而自己编写的类ClassLoaderTest 则由AppClassLoader进行加载。

    Java中各个类加载器的层次关系

    在上面已经介绍过,java的类加载器也是普通的类,ExtClassLoader和AppClassLoader均是URLClassLoader的子类,而URL的继承关系如下:


    那么AppClassLoader和ExtClassLoader为ClassLoader的子类。在上面已经已经介绍过在jvm启动时会通过sun.misc.Launcher的getLauncher方法从而获取Launcher的实例,那么在这个过程中Launcher会通过构造方法创建该类的实例。sun.misc.Launcher的构造方法如下:

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            // 创建ExtClassLoader
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
    
        try {
            // 创建AppClassPoader
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
          // 省略代码 ....
        }
    }
    
    

    通过分析sun.misc.Launcher构造方法我们知道在sun.misc.Launcher类的实例创建是会创建AppClassLoader实例和ExtClassLoader实例。同时由于两个类加载器均继承自ClassLoader,而ClassLoader中有一个ClassLoader的全局变量parent,该类类型也是ClassLoader:

    public abstract class ClassLoader {
    
        private static native void registerNatives();
        static {
            registerNatives();
        }
    
        // The parent class loader for delegation
        // Note: VM hardcoded the offset of this field, thus all new fields
        // must be added *after* it.
        private final ClassLoader parent;
    
        // 省略代码 ....
    }
    
    

    而在sun.misc.Launcher创建时,实例化ExtClassLoader和AppClassLoader时均指定其parent属性分别为null和ExtClassLoader。那么java中的类加载器的机构就如下:


    自定义类加载器

    自定义类加载器需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),实现了双亲委派机制,大体逻辑

    1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再 加载, 直接返回。

    2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器, 则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用 Bootstrap类加载器来加载。

    3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类 加载器 的findClass方法来完成类加载。还有一个方法是findClass,默认 实现是抛出异常,所以我们自定义类加载器主要是重写 findClass方法。

    接下来看一个示例,首先我们编写需要自定义类加载器加载的类,如下:

    package com.dp.jvm;
    
    import java.io.PrintStream;
    
    public class User
    {
      public void say()
      {
        System.out.println("hello");
      }
    }
    
    

    需要注意的是,该类编写完成需要在工程中删除,避免AppClassLoader加载。编译完成后将该类的class文件放置指定的目录下:


    然后编写自定义的类加载器,代码如下:

    class MyClassLoader extends ClassLoader{
    
        private final String path;
    
        MyClassLoader(String path) {
            this.path = path;
        }
    
        /**
         * 重写ClassLoader的findClass方法,获取到类的Class对象
         * @param name
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte[] byteArrayFromClassName = getByteArrayFromClassName(name);
            return defineClass(name, byteArrayFromClassName, 0, byteArrayFromClassName.length);
        }
    
        /**
         * 通过类的全限定名称获取到类的二进制数据
         * @param name
         * @return
         */
        private byte[] getByteArrayFromClassName(String name) {
            String classPath = convertNameToPath(name);
            byte[] data = null;
            int off = 0;
            int length = 0;
            try(BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(classPath))) {
                data = new byte[bufferedInputStream.available()];
                while ((length = bufferedInputStream.read(data, off, data.length - off)) > 0) {
                    off += length;
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            return data;
        }
    
        /**
         * 通过类的全限定名称获取到对应类文件的的字节码文件路径
         * @param name
         * @return
         */
        private String convertNameToPath(String name) {
            String relativePath = name.replace(".", File.separator);
            String absolutePath = path + File.separator + relativePath + ".class";
            return absolutePath;
        }
    }
    
    

    编写测试类,通过使用自定义的类加载将User加载并实例化,然后调用其say方法,如下:

    public class CustomClassLoaderTest {
    
        public static void main(String[] args) throws Exception {
            MyClassLoader myClassLoader = new MyClassLoader("F:\\test");
            Class<?> clazz = myClassLoader.loadClass("com.dp.jvm.User");
            Object o = clazz.newInstance();
            Method say = clazz.getDeclaredMethod("say");
            say.invoke(o);
        }
    }
    
    

    通过上面了实例,简单的实现了一个自定义的类加载器。接留下来了解一下类加载器的双亲委派机制。

    双亲委派机制

    JVM类加载器是有亲子层级结构的,如下图:


    需要注意的是,这里的额亲子层级结构不是指的java中的继承关系,而是每一个类加载实现类都具有一个parent全局变量,而该全局变量的类型为ClassLoader。这里可能有一个疑问,在自定义类加载器中并未看到名称parent的全局变量。这是因为这个全局变量是在ClassLoader中定义声明的。

    public abstract class ClassLoader {
    
        private static native void registerNatives();
        static {
            registerNatives();
        }
    
        // The parent class loader for delegation
        // Note: VM hardcoded the offset of this field, thus all new fields
        // must be added *after* it.
        private final ClassLoader parent;
    
        // 省略代码 ....
    
    

    还有一个问题也需要说明一下,我们自定义的类加载器的parent属性是如何设置的呢?怎么知道设置的为AppClassLoader呢?因为自定义的类加载器继承自ClassLoader,而ClassLoader中有一个无参的构造函数,如下:

    protected ClassLoader() {
       //调用有参构造函数 
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
    
    private ClassLoader(Void unused, ClassLoader parent) {
        //设置父加载器
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }
    
    

    从ClassLoader的实现来看,通过getSystemClassLoader()方法获取系统类加载器然后将其赋值给parent属性。那么来看一下getSystemClassLoader()具体实现:

    public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
        return scl;
    }
    
    // 初始化系统类加载器
    private static synchronized void initSystemClassLoader() {
        if (!sclSet) {
            if (scl != null)
                throw new IllegalStateException("recursive invocation");
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
            if (l != null) {
                Throwable oops = null;
                scl = l.getClassLoader();
                try {
                    scl = AccessController.doPrivileged(
                        new SystemClassLoaderAction(scl));
                } catch (PrivilegedActionException pae) {
                    oops = pae.getCause();
                    if (oops instanceof InvocationTargetException) {
                        oops = oops.getCause();
                    }
                }
                if (oops != null) {
                    if (oops instanceof Error) {
                        throw (Error) oops;
                    } else {
                        // wrap the exception
                        throw new Error(oops);
                    }
                }
            }
            sclSet = true;
        }
    }
    
    

    在getSystemClassLoader方法中获取sun.misc.Lanucher实例(单例),然后调用其getClassLoader方法获取系统类加载器,然后设置给parent方法。最后来看一下sun.misc.Lanucher的getClassLoader方法:

    public ClassLoader getClassLoader() {
        return this.loader;
    }
    
    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
    
        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
    }
    
    

    结合sun.misc.Lanucher的getClassLoader和构造方法可知系统类加载器就是AppClassLoader。
    在了解了jvm中类加载器的组成结构后,我们再来看一下jvm中各个类加载器的组成的结构:


    在了解了jvm中各个类加载器的层次结构之后,加下来来解析双亲委派机制就相对来说简单多了,首先从双亲委派的流程说起。

    双亲委派流程

    双亲委派流程如下:


    加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
    比如我们的PrintTest 类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托启动类加载器,顶层启动类加载器在自己的类加载路径里找了半天没找到PrintTest 类,则向下退回加载PrintTest 类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到PrintTest 类,又向下退回PrintTest 类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。
    双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载。

    那么为什么要设置双亲委派机制呢?

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

    双亲委派机制源码剖析

    双亲委派的原理体现在ClassLoader的loadClass方法中:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查类是够已经被加载
            Class<?> c = findLoadedClass(name);
    
            // 类还未加载则加载,使用双亲委派机制
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 判断当前类加载器是否设置了父加载器,设置了则
                    // 调用父加载器的loadClass进行加载,如果父加载也是ClassLoader
                    // 的子类则会再次进入该方法,判断是否有父类加载器,依次递归
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 当类加载没有设置parent父加载,那么就使用启动类加载器加载
                        // 由于启动类加载器是底层创建的实例,所以该方法会调用本地
                        // native方法
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
    
                }
    
                if (c == null) {
                    // 向上委派所有父加载器仍然没有加载到参数类,那么调用当前
                    // 类加载器进行类的加载
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    // ... 省略 
    
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    
    

    打破双亲委派

    以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?
    我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

    1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一 个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份, 因此要保证每个应用程序的类库都是独立的,保证相互隔离。比如:在tomcat容器中存在存在两个应用A和B,A使用的是Spring4,而应用B使用的是Spring5,如果使用双亲委派,那么可能会导致版本冲突从而报错,如果在版本4中不存在x方法,但是先加载了版本4的字节码,那么版本5的就不会在加载了(类限定名相同),那么在程序B中调用x方法则会抛出方法不存在异常。
    2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。我们常见的web应用中的Servlet依赖,一般在maven中依赖作用于都是provided的,web程序都是使用的容器的Serlvert,如果都是各自的那么造成类的重复加载。
    3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
    4. web容器要支持jsp的修改,我们知道,jsp文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,web容器需要支持 jsp修改后不用重启.了解jsp机制的都知道,是将jsp解析成一个对应的Servlet(就是常说的一个jsp就是一个servlet),jsp就是通过动态生成.class文件从而实现动态资源的。

    再看看我们的问题:
    Tomcat 如果使用默认的双亲委派类加载机制行不行?

    • 第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,
      默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
    • 第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
    • 第三个问题和第一个问题一样。
    • 我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

    最后

    感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

    相关文章

      网友评论

        本文标题:这篇关于JVM类加载器和双亲委派机制的笔记写的太好了,建议收藏起

        本文链接:https://www.haomeiwen.com/subject/jhtydktx.html