• JVM(六):探究类加载过程-下


    JVM(六):探究类加载过程-下

    上文说了类加载过程的5个阶段,着重介绍了各个阶段做的工作。在本文中,我们对执行加载阶段的主体进行探讨,学习类加载器的模型和逻辑,以及我们该如何自定义一个类加载器。

    定义

    前面说过加载阶段是一个可以让设计人员高度自控的模块,因为类文件的源头可以是多种多样的,代码生成、反射生成或从网络中生成等。因此类加载器作为对这些文件的处理就显得尤为重要。

    但类加载器的功能不仅如此,其还有一个重要的功能就是和一个类的全限定名唯一确定一个类。通俗来说,要说两个类是相同的,不仅其全限定名要一样,其对应的类加载器也必须相同,才能说明两个类是相等的。

    正因为类加载器的功能角色如此重要,因此虚拟机对其的实现规范也十分重视。在Java虚拟机中,对其的实现模型是双亲委派模型

    模型

    双亲委派模型

    双亲委派模型的主要执行过程示意图如上所示,其分为启动类加载器(Bootstrap Class-loader),拓展类加载器(Extension Class-loader),应用程序类加载(Application Class-loader)。

    其中启动类加载器主要负责加载 JRE 的核心类库,如 JRE 目录下的 rt.jar。但其实根据《深入分析 Java Web 技术内幕》上所说,启动类加载器并不严格符合双亲委派模型,因为Bootstrap Class-loader 并不属于 JVM 的类等级层次。Bootstrap Class-loader 是没有子类的,Extension Class-loader 也是没有父类的。不过在这里我们并不深究,只要知道有这一点就可以了。

    Extension Class-loader 主要负责加载 JRE 拓展目录 ext 下的类。

    Application Class-loader 主要负责用户类路径(Class-path)下的类,这个类加载器是使用的最多的,因为大大多数情况下,一般开发者并没有实现自定义的类加载器,那么 JVM 就会使用这个来加载类大部分类。

    执行过程

    双亲委派模型执行过程

    上图就是双亲委派模型的执行过程,当类开始加载的时候,先检查是否已经被加载过,如果没有被加载过,则调用父类的加载方法,如果父类加载失败,抛出异常,则调用自身的 findClass() 方法进行加载。

    JDK 中加载过程的源码分析:

    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 {
                        // 无父亲就调用 bootstarp 加载器来加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
            // 父加载器和 bootstarp 加载器都没有找到指定类,调用当前类的 findClass() 来完成类加载
            // 因此,自定义类加载器,就是重写 findClass() 方法
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                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;
    }
    }
    

    从源码中,我们可以看到其实符合规范要求的 双亲委派模型 的。而当我们要自定义一个类加载器的时候就是通过重写 findClass() 来实现的。

    自定义类加载器

    /**
     * 1. 自定义类加载器通过集成ClassLoader来实现,主要通过重写findClass方法
     * 2. findClass方法首先通过自定义的loadByte()方法将Class文件转换成byte[]字节流
     * 3. 然后通过defineClass()方法将其转换为Class对象
     */
    public class SelfClassLoader extends ClassLoader {
        private String classPath;
        public SelfClassLoader(String classPath) {
            this.classPath = classPath;
        }
    
        /**
         * 通过 difineClass,将一个字节数组转换为Class 对象
         * @param name
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
    
        /**
         * 根据路径将指定的文件读取为byte 流
         * @param name
         * @return
         * @throws IOException
         */
        private byte[] loadByte(String name) throws IOException {
            name = name.replaceAll("\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }
    }
    

    另一个种实现自定义类加载器的方法:

    /**
     * 1. 加载指定packageName下的类
     * 2. 用自定义类加载器进行加载,如果加载失败,再交给父加载器进行加载
     */
    public class UrlSelfClassloader extends URLClassLoader {
        private String packageName = "";
    
    public UrlSelfClassloader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> aClass = findLoadedClass(name);
        if (Objects.nonNull(aClass)){
            return aClass;
        }
        if (!packageName.startsWith(name)){
            return super.loadClass(name);
        }else {
            return findClass(name);
        }
    }
    }
    

    如何使用自定义的类加载器

    public static void main(String args[]) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("");
        Class clazz = classLoader.loadClass("");
        Object obj = clazz.newInstance();
        Method helloMethod = clazz.getDeclaredMethod("hello", null);
        helloMethod.invoke(obj, null);
    }
    

    总结

    在本文中,我们讲解了类加载器的实现模型,分析了在 JDK 中类加载器的源码实现,并根据源码中的代码实现,自定义了一个类加载器的实现。

    此外相信经过五和六两篇文章的学习,大家应该对如何将类加载入虚拟机中有了系统的理解。

    后面的文章中,我们就要进入 JVM 的内部了,从下篇文章开始,我们就开始逐步讲解 JVM 的内存布局,了解 JVM 中的各个逻辑上划分的存储结构以及其作用,欢迎各位读者浏览。

    iceWang公众号

    文章在公众号「iceWang」第一手更新,有兴趣的朋友可以关注公众号,第一时间看到笔者分享的各项知识点,谢谢!笔芯!

    本系列文章主要借鉴《深入分析 Java Web 技术内幕》和《深入理解 Java 虚拟机 - JVM 高级特性与最佳实践》。

  • 相关阅读:
    GIF文件转换为头文件工具
    深夜杂想
    swift项目第十天:网络请求工具类的封装
    swift项目第九天:正则表达式的学习
    swift项目第八天:自定义转场动画以及设置titleView的状态
    swift项目第七天:构建访客界面以及监听按钮点击
    swift项目第六天:中间发布按钮的封装以及监听点击事件
    swift项目第五天:swift中storyBoard Reference搭建主界面
    swift项目第四天:动态加载控制器
    swift项目第三天:手写代码搭建主框架
  • 原文地址:https://www.cnblogs.com/JRookie/p/11100926.html
Copyright © 2020-2023  润新知