阿飞云 · 2019年08月13日

Java内存管理-掌握类加载器的核心源码和设计模式(六)

做一个积极的人

编码、改bug、提升自己

我有一个乐园,面向编程,春暖花开!

推荐阅读

第一季

0、Java的线程安全、单例模式、JVM内存结构等知识梳理
1、Java内存管理-程序运行过程(一)
2、Java内存管理-初始JVM和JVM启动流程(二)
3、Java内存管理-JVM内存模型以及JDK7和JDK8内存模型对比总结(三)
4、Java内存管理-掌握虚拟机类加载机制(四)
5、Java内存管理-掌握虚拟机类加载器(五)
6、Java内存管理-类加载器的核心源码和设计模式(六)
7、Java内存管理-掌握自定义类加载器的实现(七)
第一季总结:由浅入深JAVA内存管理 Core Story

第二季

8、Java内存管理-愚人节new一个对象送给你(八)
【福利】JVM系列学习资源无套路赠送
9、Java内存管理-”一文掌握虚拟机创建对象的秘密”(九)
10、Java内存管理-你真的理解Java中的数据类型吗(十)
11、Java内存管理-Stackoverflow问答-Java是传值还是传引用?(十一)
12、Java内存管理-探索Java中字符串String(十二)

实战

一文学会Java死锁和CPU 100% 问题的排查技巧

分享一位老师的人工智能教程。零基础!通俗易懂!风趣幽默!
大家可以看看是否对自己有帮助,点击这里查看【人工智能教程】。接下来进入正文。

<font color='blue'>勿在流沙筑高台,出来混迟早要还的。</font>

上一篇文章介绍了类加载器分类以及类加载器的双亲委派模型,让我们能够从整体上对类加载器有一个大致的认识,本文会深入到类加载器源码分析,了解类加载器ClassLoader中核心的源码,并且分析ClassLoader中的设计思想或者设计模式!

<font color='red'> 本文地图:</font>

一、ClassLoader核心API介绍

当你要学习类的时候,首先我们要做的就是这个类API文档,下面我们从JDK API文档对 ClassLoader进行一个简单的整理。在看API之前,我们先可以大致看一下整个ClassLoader的类结构图:

ClassLoader类结构图

类 ClassLoader在java.lang 包中,是一个抽象类:

public abstract class ClassLoaderextends Object{}

类加载器是负责加载类的对象。ClassLoader 类是一个抽象类。如果给定类的二进制名称,那么类加载器会试图查找或生成构成类定义的数据。一般策略是将名称转换为某个文件名,然后从文件系统读取该名称的“类文件”。

每个 Class 对象都包含一个对定义它的 ClassLoader引用)。 (<font color='red'>这个引用很有用,在类加载器消亡的时候会卸载由它加载的类对象。</font>)

数组类的 Class 对象不是由类加载器创建的,而是由 Java 运行时根据需要自动创建。

注意:在来加载中, 有些类可能并非源自一个文件;它们可能源自其他来源(如网络),也可能是由应用程序构造的。defineClass) 方法将一个 byte 数组转换为 Class 类的实例。这种新定义的类的实例可以使用 Class.newInstance) 来创建。

类加载器所创建对象的方法和构造方法可以引用其他类。为了确定引用的类,Java 虚拟机将调用最初创建该类的类加载器的 loadClass) 方法。

如果应用程序要扩展Java 虚拟机动态加载类的方式,必须要继承ClassLoader或其子类。


tips: 二进制名称

在JAVA语言规范中定义了,任何作为 String 类型参数传递给 ClassLoader 中方法的类名称都必须是一个二进制名称。例如:

   "java.lang.String"
   "javax.swing.JSpinner$DefaultEditor"
   "java.security.KeyStore$Builder$FileBuilder$1"
   "java.net.URLClassLoader$3$1"

核心API

// 返回委托的父类加载器。 
ClassLoader getParent() 
// 使用指定的二进制名称来加载类。    
Class<?> loadClass(String name) 
// 使用指定的二进制名称查找类。 
protected  Class<?> findClass(String name) 
// 将一个 byte 数组转换为 Class 类的实例。
protected  Class<?> defineClass(String name, byte[] b, int off, int len) 
// 链接指定的类 
protected  void resolveClass(Class<?> c) 
          
// 其他的自行查资料学习     

将完 ClassLoader后在看一个它的子类 URLClassLoader,它重写了ClassLoader中的一些方法,并且它的子类中有我们比较关注的AppClassLoaderExtClassLoader

<font color='red'>URLClassLoader:该类加载器用于从指向 JAR 文件和目录的 URL 的搜索路径加载类和资源</font>。这里假定任何以 '/' 结束的 URL 都是指向目录的。如果不是以该字符结束,则认为该 URL 指向一个将根据需要打开的 JAR 文件。

二、ClassLoader核心源码分析

在看源码之前在回顾一下ClassLoader的作用,这样也能让我们知道从什么地方开始阅读源码!

回顾: 我们编写的Java程序在编译后,必须加载到JVM中才能运行,类装载器所做的工作就是把class文件从指定的地方读取到内存中,JVM在加载类的时候,都是通过类装载器ClassLoaderloadClass()方法来加载class的,loadClass使用双亲委派模式。具体看下面源码分析:

/**
     * 使用指定的二进制名称来加载类。此方法使用与 loadClass(String, boolean) 方法相同的方式搜索类。
     * Java 虚拟机调用它来分析类引用。调用此方法等效于调用 loadClass(name, false)。
     * @throws  ClassNotFoundException 如果类没有发现,抛出异常
     */
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
/**
     * 使用指定的二进制名称来加载类。此方法的默认实现将按以下顺序搜索类:
     *
     * 1.调用 findLoadedClass(String) 来检查是否已经加载类。
     *
     * 2.在父类加载器上调用 loadClass 方法。如果父类加载器为 null,则使用虚拟机的内置类加载器。
     *
     * 3.调用 findClass(String) 方法查找类。
     *
     * 如果使用上述步骤找到类,并且 resolve 标志为真,则此方法将在得到的 Class 对象上调用 resolveClass(Class) 方法。
     *
     * 鼓励用 ClassLoader 的子类重写 findClass(String),而不是使用此方法,此方法是一个空方法。
     *
     * @param name  类的二进制名称
     * @param resolve  如果该参数为 true,则分析这个类
     * @return  得到的 Class 对象
     * @throws ClassNotFoundException  如果无法找到类
     */
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        // 除非被重写,否则这个方法默认在整个装载过程中都是同步的,使用了synchronized(也就是线程安全的)
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //  检查类是否已经被加载  ,如果加载过 直接返回该Class类型的对象
            Class<?> c = findLoadedClass(name); // 说明:1
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 类未加载 返回 null ,则判断 这个 classLoaer 是否有父类
                    // 父类不等于 null 就去父类去加载,否则去 BootStrap中去加载,双亲委派模式!
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                    //如果找不到类,则抛出ClassNotFoundException
                    //来自非null父类加载器
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    // 如果都没有找到的话,则从findCLass中去找,这个findClass ,子类去复写这个方法
                    // 默认就是实现自定义的classLoader
                    long t1 = System.nanoTime();

                    c = findClass(name);// 说明:2

                    // 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;
        }
    }

    /**
     * 说明:1
     * 如果 Java 虚拟机已将此加载器记录为具有给定二进制名称的某个类的启动加载器,
     * 则返回该二进制名称的类。否则,返回 null。
     * @param name
     * @return
     */
    protected final Class<?> findLoadedClass(String name) {
        // checkName(name) 如果名称为null或可能是有效的二进制名称,则返回true
        if (!checkName(name))
            return null;
        return findLoadedClass0(name);
    }

    private native final Class<?> findLoadedClass0(String name);

    /**
     * 说明:2  
     * 使用指定的二进制名称查找类。此方法应该被类加载器的实现重写,该实现按照委托模型来加载类。
     * 在通过父类加载器检查所请求的类后,此方法将被 loadClass 方法调用。
     * 默认实现抛出一个 ClassNotFoundException。 子类去重写!
     * @param name   类的二进制名称 
     * @return
     * @throws ClassNotFoundException
     */
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
 

AppClassLoader 也重写了loadClass(),省略了很多代码!因为AppClassLoader 继承自URLClassLoader,URLClassLoader中重写了 findClass()方法!具体感兴趣的伙伴可以自行看相关源码!

 static class AppClassLoader extends URLClassLoader {
       // 省略其他代码....
        public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
            int var3 = var1.lastIndexOf(46);
            // 省略其他代码....
            } else {
                return super.loadClass(var1, var2);
            }
        }
     // 省略其他代码....
 }

看了上面的源码,主要对 loadClass进行分析!

第一: synchronized (getClassLoadingLock(name))默认在整个装载过程中都是线程安全的

synchronized (getClassLoadingLock(name)) 看到这行代码,我们能知道的是,这是一个同步代码块,那么synchronized的括号中放的应该是一个对象。看一下具体的源码:

/**
* 返回类加载操作的锁定对象。
* private final ConcurrentHashMap parallelLockMap;
**/
protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
}

我们来看getClassLoadingLock(name)方法的作用是什么:代码中用到变量parallelLockMap实际上是一个ConcurrentHashMap ,根据这个变量的值进行不同的操作,如果这个变量是Null,那么直接返回this,如果这个属性不为Null,那么就新建一个对象,然后在调用一个putIfAbsent(className, newLock)方法来给刚刚创建好的对象赋值,(稍后介绍此方法的作用)。那么这个parallelLockMap变量又是哪来的那,我们发现这个变量是ClassLoader类的成员变量。

  private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
          // 根据一个属性ParallelLoaders的Registered状态的不同来给parallelLockMap 赋值
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
         // 其他代码省略 ...
        } else {
            // no finer-grained lock; lock on the classloader instance
            // 没有更精细的锁;锁定类加载器实例
            parallelLockMap = null;
           // 其他代码省略 ...
        }
    }

从构造函数中看到,parallelLockMap的值是根据 ParallelLoaders.isRegistered 去进行判断并赋值的,那么,ParallelLoaders是在什么地方赋值的呢?在ClassLoader类中包含一个静态内部类private static class ParallelLoaders,在ClassLoader被加载的时候这个静态内部类就被初始化。(<font color='blue'>整个类的源码没怎么看懂,比较底层深入了</font>)

     /**
     *封装了并行的可装载的类型的集合
     */
    private static class ParallelLoaders {
        private ParallelLoaders() {}

        //一组并行的加载器类型
        private static final Set<Class<? extends ClassLoader>> loaderTypes =
            Collections.newSetFromMap(
                new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
        static {
            synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
        }

        /**
         * 将给定的类加载器类型注册为并行capabale。
         */
        static boolean register(Class<? extends ClassLoader> c) {
            synchronized (loaderTypes) {
                if (loaderTypes.contains(c.getSuperclass())) {
                   //将类加载器注册为并行功能
                     //当且仅当它的所有超类都是。
                     //注意:给定当前的类加载序列,如果
                     //直接超级类是并行的,
                     //所有超级上级必须也是。
                    loaderTypes.add(c);
                    return true;
                } else {
                    return false;
                }
            }
        }
        static boolean isRegistered(Class<? extends ClassLoader> c) {
            synchronized (loaderTypes) {
                return loaderTypes.contains(c);
            }
        }
    }

上面这一整段比较乱,简单整理总结:

首先,在ClassLoader类中有一个静态内部类ParallelLoaders,他会指定的类的并行能力,如果当前的加载器被定位为具有并行能力,那么他就给parallelLockMap定义,就是new一个 ConcurrentHashMap(),那么这个时候,我们知道如果当前的加载器是具有并行能力的,那么parallelLockMap就不是Null,这个时候,我们判断parallelLockMap是不是Null,如果他是null,说明该加载器没有注册并行能力,那么我们没有必要给他一个加锁的对象,getClassLoadingLock方法直接返回this,就是当前的加载器的一个实例。如果这个parallelLockMap不是null,那就说明该加载器是有并行能力的,那么就可能有并行情况,那就需要返回一个锁对象。然后就是创建一个新的Object对象,调用parallelLockMapputIfAbsent(className, newLock)方法,这个方法的作用是:首先根据传进来的className,检查该名字是否已经关联了一个value值,如果已经关联过value值,那么直接把他关联的值返回,如果没有关联过值的话,那就把我们传进来的Object对象作为value值,className作为Key值组成一个map返回。然后无论putIfAbsent方法的返回值是什么,都把它赋值给我们刚刚生成的那个Object对象。 这个时候,我们来简单说明一下getClassLoadingLock(String className)的作用,就是: 为类的加载操作返回一个锁对象。为了向后兼容,这个方法这样实现:如果当前的classloader对象注册了并行能力,方法返回一个与指定的名字className相关联的特定对象,否则,直接返回当前的ClassLoader对象。[摘自:参考资料2]

第二:findLoadedClass(name),查找类是否已经被加载过,如果加载过 直接返回该Class类型的对象。如果没有被加载则继续第三的操作!

第三:c = findBootstrapClassOrNull(name);c = parent.loadClass(name, false);如果父加载器不为空,那么调用父加载器的loadClass方法加载类,如果父加载器为空,那么调用虚拟机的加载器来加载类。

如果以上两个步骤都没有成功的加载到类,进入第四。

第四:c = findClass(name);使用自定义的findClass(name)方法来加载类。

这个时候,我们已经得到了加载之后的类,那么就根据resolve的值决定是否调用resolveClass方法。进入第五!

第五:resolveClass(c); 链接指定的类。这个方法给Classloader用来链接一个类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。否则,这个类将被按照 Java™规范中的Execution描述进行链接……

三、ClassLoader设计思想解析

在ClassLoader中虽然是叫双亲委派(父亲委派)机制,但真实的实现不是以继承的关系进行实现,而是都使用组合关系来复父加载器的代码。通过getParent() 这个方法设置父类加载器的。这个其实在面向对象(OO)设计原则中有一条就是: 多用组合,少用继承

类加载器中用到了模板方法模式:模板方法模式用于定义构建某个对象的步骤与顺序,或者定义一个算法的骨架

模板方法模式的使用的方式,给子类足够的自由度,提供一些方法供子类覆盖,去实现一些骨架中不是必须但却可以有自定义实现的步骤。模板方法模式是一种基础继承的代码复用技术。如ClassLoader中的findClass方法!

public abstract class ClassLoader {
    
    //这里就是父类算法的定义
    protected synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
        if (parent != null) {
            c = parent.loadClass(name, false);
        } else {
            c = findBootstrapClass0(name);
        }
        } catch (ClassNotFoundException e) {
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
    }
    //这里留了一个方法给子类选择性覆盖
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
    }
}

在来回顾:在ClassLoader中定义的算法顺序是。

1、首先看是否有已经加载好的类。

2、如果父类加载器不为空,则首先从父类类加载器加载。

3、如果父类加载器为空,则尝试从启动加载器加载。

4、如果两者都失败,才尝试从findClass方法加载。

四、ClassLoader核心总结

本文从源码和设计模式的角度对ClassLoader进行分析和学习,更加深入的学习了ClassLoader,也方便后续我们自定义的类加载器,下一篇介绍如果实现一个自定义的类加载器!


tips : 常见异常:

附录:

ClassNotFoundException 这是最常见的异常,产生这个异常的原因为在当前的ClassLoader 中加载类时,未找到类文件。

NoClassDefFoundError 这个异常是因为 加载到的类中引用到的另外类不存在,例如要加载A,而A中盗用了B,B不存在或当前的ClassLoader无法加载B,就会抛出这个异常。

LinkageError 该异常在自定义ClassLoader的情况下更容易出现,主要原因是此类已经在ClassLoader加载过了,重复的加载会造成该异常。


五、参考资料

《JDK API 文档》

深度分析 Java 的 ClassLoader 机制(源码级别):http://blog.jobbole.com/96145/

<font color='red'>备注: 由于本人能力有限,文中若有错误之处,欢迎指正。</font>


谢谢你的阅读,如果您觉得这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到!祝你每天开心愉快!


<center><font color='red'>Java编程技术乐园</font>:一个分享编程知识的公众号。跟着老司机一起学习干货技术知识,每天进步一点点,让小的积累,带来大的改变!</center>
<p/>
<center><font color='blue'>扫描关注,后台回复【资源】,获取珍藏干货! 99.9%的伙伴都很喜欢</font></center>
<p/>

image.png | center| 747x519

<center>© 每天都在变得更好的阿飞云</center>

推荐阅读
关注数
1
文章数
11
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息