美文网首页
JVM类加载器及双亲委派模型

JVM类加载器及双亲委派模型

作者: 只会敲代码的键盘手 | 来源:发表于2020-05-31 23:46 被阅读0次

    1.前言

    前面讲解了类的加载机制,对于JVM类的加载过程有了简单的了解,这一章接着学习类加载的一些细节,类加载器和双亲委派模型

    2.目录

    目录

    3.类加载器

    在JVM中有三类ClassLoader构成:启动类(或根类)加载器(BootstrapClassLoader),扩展类加载器(ExtClassLoader),应用类加载器(AppClassLoader).不同类加载器负责加载不同区域的类

    类加载器
    • 启动类加载器:这个加载器不是一个Java类,而是由底层的c++实现,负责将存放在JAVA_HOME下lib目录中的类库,比如rt.jar.因此,启动类加载器不属于Java类库,无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可
    • 扩展类加载器:由sun.misc.Launcher$ExtClassLoader实现,负责加载JAVA_HOME下libext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器
    • 应用类加载器:由sun.misc.Launcher$AppClassLoader实现的.由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以也叫系统类加载器.它负责加载用户类路径上所指定的类库,可以被直接使用.如果未自定义类加载器,默认为该类加载器

    3.1.类加载器的初始化

    除启动类加载器外,扩展类加载器和应用类加载器都是通过类sun.misc.Launcher进行初始化,而Launcher类则由根类加载器进行加载.相关代码如下:

    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);
        }
        // 设置上下文类加载器
        Thread.currentThread().setContextClassLoader(this.loader);
        ...
    }
    

    4.双亲委派模型

    双亲委派模型:当一个类加载器接收到类加载请求时,会先请求其父类加载器加载,依次递归,当父类加载器无法找到该类时(根据类的全限定名称),子类加载器才会尝试去加载

    双亲委派模型

    loadClass源码:
    ClassLoader类是一个抽象类,但却没有包含任何抽象方法.继承ClassLoader类并重写findClass方法便可实现自定义类加载器.但如果破坏上面所述的双亲委派模型来实现自定义类加载器,则需要继承ClassLoader类并重写loadClass方法和findClass方法

    ClassLoader的源码如下:

    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) {
                        //如果当前类没有被加载且父类加载器不为null,则请求父类加载器进行加载操作
                        c = parent.loadClass(name, false);
                    } else {
                       //如果当前类没有被加载且父类加载器为null,则请求根类加载器进行加载操作
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
                if (c == null) {
                    long t1 = System.nanoTime();
                   //如果父类加载器加载失败,则由当前类加载器进行加载,
                    c = findClass(name);
                    //进行一些统计操作
                   // ...
                }
            }
            //初始化该类
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    

    使用双亲委派模型的原因:
    双亲委派模型是为了保证Java核心库的类型安全,所有Java应用都至少需要引用java.lang.Object类,在运行时这个类需要被加载到Java虚拟机中.如果该加载过程由自定义类加载器来完成,可能就会存在多个版本的java.lang.Object类,而且这些类之间是不兼容的.通过双亲委派模型,对于Java核心库的类的加载工作由启动类加载器来统一完成,保证了Java应用所使用的都是同一个版本的Java核心库的类,是互相兼容的

    4.1.上下文类加载器

    子类加载器都保留了父类加载器的引用,但如果父类加载器加载的类需要访问子类加载器加载的类该如何处理?最经典的场景就是JDBC的加载
    JDBC是Java制定的一套访问数据库的标准接口,它包含在Java基础类库中,由根类加载器加载.而各个数据库厂商的实现类库是作为第三方依赖引入使用的,这部分实现类库是由应用类加载器进行加载的

    连接MySql

    //加载驱动程序
    Class.forName("com.mysql.jdbc.Driver");
    //连接数据库
    Connection conn = DriverManager.getConnection(url, user, password);
    

    DriverManager由启动类加载器加载,它使用到的数据库驱动com.mysql.jdbc.Driver是由应用类加载器加载的,这就是典型的由父类加载器加载的类需要访问由子类加载器加载的类

    DriverManager类连接的实现源码:

    //建立数据库连接底层方法
    private static Connection getConnection(
            String url, java.util.Properties info, Class<?> caller) throws SQLException {
        //获取调用者的类加载器
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            //由启动类加载器加载的类,该值为null,使用上下文类加载器
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }
        //...
        for(DriverInfo aDriver : registeredDrivers) {
            //使用上下文类加载器去加载驱动
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    //加载成功,则进行连接
                    Connection con = aDriver.driver.connect(url, info);
                    //...
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }
            } 
            //...
        }
    }
    
    callerCL = Thread.currentThread().getContextClassLoader();
    

    这行代码从当前线程中获取ContextClassLoader,而ContextClassLoader在哪里设置呢?就是在上面的Launcher源码中设置的

    // 设置上下文类加载器
    Thread.currentThread().setContextClassLoader(this.loader);
    

    这样一来,所谓的上下文类加载器本质上就是应用类加载器.因此,上下文类加载器只是为了解决类的逆向访问提出来的一个概念,并不是一个全新的类加载器,本质上是应用类加载器

    4.2.自定义类加载器

    自定义类加载器只需要继承java.lang.ClassLoader类,然后重写findClass(String name)方法即可,在方法中指明如何获取类的字节码流

    如果要破坏双亲委派规范的话,还需重写loadClass方法(双亲委派的具体逻辑实现).但不建议这么做。

    public class TestClassLoader extends ClassLoader {
        private String mClassPath;
        public TestClassLoader() {
        }
        public TestClassLoader(String classPath) {
            mClassPath = classPath;
        }
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte[] classData = getClassData(name);
            if (classData == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name,classData,0,classData.length);
            }
        }
        private byte[] getClassData(String name) {
            String path = convert2Path(name);
            try {
                FileInputStream fis = new FileInputStream(path);
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                byte[] bytes = new byte[1024];
                int length = 0;
                while ((length = fis.read(bytes)) != -1) {
                    byteArrayOutputStream.write(bytes,0,length);
                }
                return byteArrayOutputStream.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
        private String convert2Path(String name) {
            return mClassPath + File.separatorChar
                    + name.replace('.', File.separatorChar) + ".class";
        }
    }
    

    main()中测试TestClassLoader

    public static void main(String[] args) {
        String testPath = Test.class.getProtectionDomain().getCodeSource().getLocation().getFile();
        System.out.println(testPath);
        TestClassLoader testClassLoader = new TestClassLoader(testPath);
        Class<?> aClass;
        try {
            aClass = testClassLoader.loadClass(TestClassLoader.class.getPackage().getName() + "." + TestClassLoader.class.getSimpleName());
            System.out.println(aClass.newInstance().toString());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
    
    打印结果

    上面的代码通过重写了findClass获取class的路径便实现了自定义的类加载器,
    当JDK提供的类加载器实现无法满足我们的需求时,才需要自己实现类加载器。比如,插件化和热更新

    5.总结

    这一章主要讲解了类加载器和双亲委派模型,其实理解起来也并不复杂,但是在实际的场景中,必须对其机制有足够的了解,才可以实现插件化和热更新类似的需求,这也是android中的一大黑科技


    原文地址:
    https://www.choupangxia.com/2019/10/29/interview-jvm-gc-04/

    相关文章

      网友评论

          本文标题:JVM类加载器及双亲委派模型

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