一篇文章彻底弄懂类加载--源码级别讲解!!!!!!

在看文章之前,需要大家理解java程序、.java、.class、类加载器之间的关系。
我这里通过一张图将我所理解的他们之间的关系画一下。

在这里插入图片描述

JVM是什么?

在上面所画的图中,我们基本能够了解到,我们的java程序的运行是通过一系列的class文件所支持的。而且一个程序的开发中,程序的运行都是由jvm所实现的,但是jvm是由sun公司所开发的,我们无法去修改其内部实现,除非你自己写一个jvm,但是一般人不会去干这事,因此,我们就需要去了解jvm的相关运行原理。

jvm(java virtual Machine)是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。也就是说,jvm是一个虚拟计算机,为什么要弄一个这样的虚拟计算机来呢?Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。
常见的java虚拟机有哪些呢?从事java开发工作的人员第一时间就是想到的hotspot,是的,hotspot是sun/oracleJDK和OpenJDK中的默认虚拟机,也是目前使用范围最广的虚拟机。但是除了hotspot,还有其他几款虚拟机,BEA JRockit、J9等。他们在各个领域都有一定的优势,具体的细节,需要各位自己去了解。

jvm的基本架构(hotspot)

我这里还是通过一张图来说明:
在这里插入图片描述
通过上面的图我们可以知道,类加载引擎是一个jvm的入口,他的主要作用就是加载class文件的。在class文件格式与执行引擎这部分里,用户的程序能直接参与的内容并不多,class文件以何种格式存储,类型何时加载、如何链接,以及虚拟机如何执行字节码指令等,都是由虚拟机直接控制的行为,用户程序无法对其进行改变。能通过程序进行操作的,主要是字节码生成与类加载器这两部分的内容。所以很多的大体量的互联网产商都喜欢问类加载和字节码生成这两块的内容,因为决定一个程序员的功底的除了开发经验之外就是看他对底层原理理解有多深刻。其中字节码生成这一部分比较晦涩难懂,大家都会比较难以接受,因此,很多产商主要问的也是类加载这一部分。好了,通过上面一系列的解释,我们应该基本都知道了为什么我们的学习类加载器的相关内容了。前戏结束了,开始正题!

类加载器

我们现在都知道了类加载器是用来加载class文件的,那么它是怎么加载class文件的呢?我们都知道在java开发中如果要读取一个文件的内容,jdk给了相关类去实现,他的基本流程就是:找到文件的目录------>获取文件流------>通过流读取文件内容。类加载也是如此,也是需要先知道文件的目录或者是地址,然后获取文件内容,再读给jvm中,然后jvm对读取的文件做一些相关的操作。

我们在做一些java开发的时候,有一些类不需要我们主动去加载就可以直接拿来用,这是因为sun公司提供了给开发人员用于基本开发的一些功能类让我们使用,而这些类的是在一个程序运行起来之前就被jvm通过类加载器加载进了运行时数据区中,然后,我们开发人员就只需要拿来用就行了。

package utils;


public class test {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

我们观察一下上面这个程序,一个经典的案例,任何一个开发人员基本都有的一个过程,helloworld程序。上面我们创建了一个test类,这个类运行之后,会在控制台输出“hello world”,其中public、class、void是java的关键字,是java开发语法中我们会学习的,再观察,还有String[]、System,他们分别是String数组和System类,但是我们观察程序的上方,并没有import这两个类,但是我们的程序的运行却没有任何问题,那么,这个java程序在运行的时候是怎么找到这两个类的呢?答案就在我上面说的jvm会在一个java程序运行的时候会将sun公司提供给我们开发人员的一些工具类自动装载到内存区中,因此,在开发中我们可以直接拿来用,也不需要进行导包,而这些类一般存在于java.lang包下。

好了,我们总结一些上面知识的重点:

一个文件被加载到内存的步骤
获取文件目录地址---->获取文件流------>读取文件内容到内存中

类加载器是干嘛的?
用于加载class文件,也就是java所说的类,其实class文件是一个文本文件,也就是说我们程序员所写的逻辑程序都是放在一个文本文件中的,然后通过读取到jvm中,jvm通过解析字节码、运行相关的jvm指令来达到一些逻辑功能。而加载这些文件的功能由类加载完成。

那么我就提出一个问题,既然一个文件被加载到内存的步骤都是一样的, 类加载器的功能就是加载类也就是class文件,那么同样的是不是也要找到相关的路径或者目录,然后获取流,最后再读取内容到jvm的运行时内存区呢?

是的,类加载器同样也是在干一样的事情,只不过这些事情在一个程序运行前就被做完了,这里还要普及一个概念,就拿我们上面说的经典案例来说,在运行上面这个类中的main方法中的代码的之前,jvm会加载很多类,最后再运行你写的System.out.println(“Hello World”); 。既然类加载也需要找到相关的路径和目录才能加载类,那么这些路径是怎么配的呢?大家不知道还记不记得在配置jdk的环境变量的时候,我们需要配置相关的路径,其实这里配置的路径也算是配置相关类加载器的路径。然后类加载器就通过这个路径去加载相关的类文件。这里我就不讲解相关路径的配置了。如果对路径配置还不懂的大家可以去看看环境变量的配置

讲再多的概念性的东西,如果没有真实存在的东西或者实验做支撑,总显得那么的薄弱。下面我们就由浅入深地对类加载的相关的源码进行讲解,希望大家能够通过这篇文章对类加载有一个大概的了解。这也是博主的初衷!

在讲解之前先贴一张我们下面所涉及类的一个表:

描述
ClassLoader类加载器是负责加载类的对象。ClassLoader=是一个抽象类。给定一个类的<a * href="#name">二进制名称,类加载器应该尝试定位或生成构成类定义的数据。 典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的 类文件。
Launchersun.misc.Launcher类是java的入口,在启动java应用的时候会首先创建Launcher类,创建Launcher类的时候会准备应用程序运行中需要的类加载器。
URLClassLoader这个类加载器用于从引用 JAR 文件和目录的 URL 搜索路径加载类和资源。任何以“/”结尾的 * URL 都被认为是指一个目录。否则,URL *被假定为引用将根据需要打开的 JAR 文件。
ExtClassLoaderjava编写,加载扩展库,如classpath中的jre ,javax.*或者 java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。
AppClassLoaderjava编写,加载程序所在的目录,如user.dir所在的位置的class
Class类的实例表示正在运行的 Java 应用程序中的类和接口。枚举是一种类,注释是一种接口。每个数组也属于一个类,该类反映为 {@code Class} 对象 ,该对象由具有相同元素类型和维数 * 的所有数组共享。 Java 原始类型({@code boolean}、* {@code byte}、{@code char}、{@code short}、* {@code int}、{@code long}、{@code float} 和* {@code double}) 和关键字 {@code void} 也 * 表示为 {@code Class} 对象。

Classloader

它是URLClassLoader,ExtClassLoader,AppClassLoader的超类,位于java.lang包下的一个类,因此我们可以不需要导包就可以直接在程序中使用,那我们就来看看这个类的功能有哪些,先贴一张jdk1.8中文版对classloader类的一个描述:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个中文版翻译过来让我看的很是难受,我就通过源码让大伙来一点点的理解这个类的作用,首先我们看构造器。

 //两个构造的方法
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;
        }
    }

我们先看带两个参数的构造器,两个参数分别是,就是给一些属性进行赋值的操作,其中我们需要关注的一个参数是 this.parent 这个参数在之后会有大作用,好了,看了带两个参数的构造器,再看看带一个参数的构造器。

//一个参数的构造
protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    } 
    
  private static Void checkCreateClassLoader() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        return null;
    }

一个参数的构造器调用了带两个参数的构造器,其中参数由checkCreateClassLoader()方法来产生,这个方的首先判断用户是否设置了安全管理器,如果有安全管理器,那就调用安全管理器中的checkCreateClassLoader()方法去判断是否允许线程是否允许创建一个新的classloader类,最后再返回一个null值,好家伙,前面对安全管理器的一连串的操作,我们暂时先放一下,之后还会讲解,我们要重点关注于它的返回值是个空值,也就是说,一个参数的构造器将Void 赋值为null,ClassLoader 参数赋值为给this.parent 。最后我们再看看无参构造器。

protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

它调用了两个参数的构造器,第一个参数还是null值,那么我们重点来看看第二个方法。

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(),它首先调用了initSystemClassLoader()方法,这个方法里面干了什么了呢?首先通过sun.misc.Launcher.getLauncher();方法拿到launcher类对象,然后从launcher类中拿到一个类加载类对象赋值给scl属性,这样scl就有一个类加载器对象了,然后getSystemClassLoader()再把这个对象返回作为构造器的参数。

好了我们已经看完了这个类的所有的构造器,由此了解到构造器是给这个类中的属性进行赋值。具体的功能实现得有功能来实现。classloader这个类的主要的方法有两个loadClass()defineClass()

其中loadClass方法的作用加载具有指定二进制名称的类。 此方法以与 {@link * #loadClass(String, boolean)} 方法相同的方式搜索类。它由 Java 虚拟机调用以解析类引用。调用此方法等效于调用。这是从的注释翻译过来的,可能不是太能够理解,因此贴上jdk中文版的解释。
在这里插入图片描述

defineClass的作用将字节数组转换为类 Class 的实例,带有可选的 ProtectionDomain。如果域为 null,则默认域将分配给 {@link #defineClass(String, byte[], * int, int)}文档中指定的 类。在类可以使用之前,它必须被解析。

在这里插入图片描述
通过上面的内容精简一些地对两个方法作用的描述如下:

  • loadClass
    通过指定的全限定类名加载class
  • defineClass
    将class二进制内容转换成class对象

loadClass方法的实现我们还需要去看看,因为,它涉及到类加载的一个机制,也就是双亲委派机制,我们这里不对defineclass方法进行详细解释,如果感兴趣的小伙伴可以自行去查阅相关资料。贴上loadClass方法的源码:

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 {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

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

一步一步分析,这个方法有两个参数,第一个参数是要加载的类的全限定名,第二个参数是一个boolean对象,首先通过findLoadedClass(name);方法来检查我们要加载的类是否已经被加载过,如果加载过,则会返回这个类的Class对象。

然后我们来看下面这段从方法中截取下来的代码,不知道大家还记得上面讲解构造器的时候,我要大家注意的一个属性 this.parent吗?这里就会被使用到了,那么我们就来分析一下这段代码,它的意思是让之前我们赋值给parent的类加载去加载这个类对象, parent为null那么就调用findBootstrapClassOrNull(name);,这个方法调用到最后调用的是一个native的方法,也就是说,它的实现是由c/c++实现的,具体如何实现的,可以不用过多的去关注。我们只需要知道它通过c/c++去加载这个类,如果没有加载到,返回一个null,如果加载到了则返回这个类的Class对象。

  if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }

再来看一下方法的后续操作,如果通过findBootstrapClassOrNull(name)还是没有找到Class对象,那么他就会调用这个类中的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();
                }

最后如果resolve这个参数的值为true则会调用resolveClass()方法,这个方法目的就是去链接一个Class对象,如果链接的,链接的效果是什么,博主了解的不是很多,如果有懂的小伙伴,可以在评论区告诉我。不过不懂这个链接方法没关系,不影响我们的整体的思路。

if (resolve) {
                resolveClass(c);
            }
            return c;

好了,我们讲完了ClassLoader类。在讲解这个类的源码的时候设计到了一个加载机制,那就是双亲委派模型下面我们先对这个机制做一个了解,然后去了解接下来的几个类会更加的容易理解。

双亲委派模型

通过从《深入理解Java虚拟机》引用一段话来对这个模型来做一个解释:

如果一个类加载器收到了类加载器的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

其实这个模型的实现就是上面我们loadClass方法中的一段代码。也就是说,我们类的加载过程是按照这个模型来进行加载的。但是万事没有绝对,在以往也有过破坏双亲委派机制的例子。第一次破坏是在双亲委派机制出现之前—即jdk1.2面世以前的远古时代。由于双亲委派机制在jdk1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在java第一个版本中就已经存在了,面对已经存在的用户自定义类加载的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass()中编写代码。我们上面在分析ClassLoader源码的时候就已经有过相应的了解,因此,为了保证双亲委派机制的完整性,我们在编写类加载器逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。

双亲委派机制的第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派机制很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器去进行加载),基础类型之所以被称为“基础”,是因为它们总是作为用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?

这并非不可能的事情,一个典型的例子便是JNDI(Java Naming Directory Interface)服务,JNDI现在已经是java的标准服务,它的代码由启动类加载器来完成加载(在JDK1.3时加入到rt.jar的),肯定属于java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供接口SPI(Service Provider Interface)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?

为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

好了,有了上面这些知识的讲解,不知道大家对类加载器有没有一定的认识,其实类加载器也是一个类。这个类的工作就是去加载指定的路径下的class文件。

最后再贴上双亲委派机制的流程图:

在这里插入图片描述

URLClassLoader

这个类加载器用于从引用 JAR 文件和目录的 URL 搜索路径加载类和资源。任何以“/”结尾的 URL 都被认为是指一个目录。否则,URL 被假定为引用将根据需要打开的 JAR 文件。创建 URLClassLoader 实例的线程的 AccessControlContext 将在随后加载类和资源时使用。默认情况下,加载的类仅被授予访问创建 URLClassLoader 时指定的 URL 的权限。

我们这里对他的基本功能有了一个了解,我们对于这个类可以不用很详细的去解释,其实只需要我们知道它的几个构造器就成。我贴上源代码:

 public URLClassLoader(URL[] urls, ClassLoader parent) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        this.acc = AccessController.getContext();
        ucp = new URLClassPath(urls, acc);
    }
 URLClassLoader(URL[] urls, ClassLoader parent,
                   AccessControlContext acc) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        this.acc = acc;
        ucp = new URLClassPath(urls, acc);
    }
 public URLClassLoader(URL[] urls) {
        super();
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        this.acc = AccessController.getContext();
        ucp = new URLClassPath(urls, acc);
    }
URLClassLoader(URL[] urls, AccessControlContext acc) {
        super();
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        this.acc = acc;
        ucp = new URLClassPath(urls, acc);
    }
 public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        acc = AccessController.getContext();
        ucp = new URLClassPath(urls, factory, acc);
    }

对于上面的构造器,相比较它的父类ClassLoader的构造方法,它提供了一个三个参数的构造器,其中一个参数为一个URL对象的数组、一个为ClassLoader、还有一个是AccessControlContext ,其中URL对象和ClassLoader这两个对象大家都有所了解,我这里对就只对AccessControlContext 进行一些解释,这个类用于根据它封装的上下文做出系统资源访问决策。说简单点就是用于安全管理的一个类,安全管理怎么实现的,我们在这里不过多的描述,如果感兴趣的小伙伴可以去查询相关资料,它和AccessController这个类的功能有点类似。在三个参数的构造器中首先就调用通过super(parent);调用了父类的一个参数的构造方法,然后也就是一些赋值操作。这里需要我们关注的点也是这个父类构造器调用方法,这个方法设置了this.parent这个属性的引用。

对于URLClassLoader类做个总结,它继承自ClassLoader类,在ClassLoader基础上扩展了一些功能,如:安全管理、路径的管理等。它也重写了findClass方法,因此它也有类加载的功能。

好了,有了上面知识做铺垫,接下来要讲解的两个类就非常的容易理解了。

ExtClassLoader和AppClassLoader

这两个类继承自URLClassLoader类,那么它们也就有了父类所拥有的一些功能,但是,sun公司的工程师将它们设计出来并不是做跟URLClassLoader同样的功能的。老规矩,先上两个类的构造方法:

private static URLStreamHandlerFactory factory = new Launcher.Factory();

 AppClassLoader(URL[] var1, ClassLoader var2) {
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }
public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }

通过上面这两个构造方法,我们知道,他们都是调用了其父类中的

URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory)

的构造方法,其中URLStreamHandlerFactory 是传递Launcher类中的私有静态类factory ,但是我们的重点不在这里,我们关注点要一直围绕一个属性this.parent这个属性来,那让我们来看看这两个类的构造方法中是如何给这个属性赋值的,先看AppClassLoader的构造方法,他的第二个参数,也就是给this.parent的赋值的ClassLoader对象,是需要我们自己去赋值,也就是说可以指定赋值,但是,我们再仔细观察一下,这个构造方法的作用范围是默认的,也就是说,我们无法在外部去new一个这个对象,我们可以大胆地推测一下,它必然在Launcher类中有相关的一些初始化这个类的操作,我这个推测真不真实,等会我们就知道了。

我们已经知道了this.parent的赋值了,那么它的加载路径是怎么确定的呢?让我们来看一个方法,设置了AppClassLoader的加载类路径,它就是通过拿java.class.path系统变量中的路径,也就是我们最开始提到过在安装jdk的时候配置的环境变量中的设计的,它对应的也就是CLASSPATH中配置的路径,这样,我们也就知道了AppClassLoader的加载类的路径了。同时还提供了用户拿到AppClassLoader的一个方法,能够让用户在外面得到这个对象。

 public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }

同时,AppClassLoader竟然重写了loaderClass方法,上面我们在双亲委派模型的时候说过,我们类加载器的双亲委派机制的实现就是在loaderClass方法中实现的,但是这里AppClassLoader竟然重写了这个方法,sun公司推荐我们不去重写这个方法,而是去重写findClass方法,显而易见,sun公司的工程师不可能犯这种低级错误,那我们有能猜测一下,我们之前说过的一个上下文线程加载器就是一个破坏了双亲委派机制的一个加载器,那么,我们可以这么想,这个类加载器是不是就是拿来当上下文线程加载器的呢?答案肯定显而易见是的。而上下文线程加载器的设置是在Launcher中设置的,具体操作我们放到Launcher中去讲解。

好了,对于AppClassLoader类,我们基本就只需要了解这么多了。然后就开始了解ExtClassLoader。通过观察它的构造方法,我们知道它并没有给
loaderClass进行赋值,也就是说它没有父类加载器(严格来说,它也是有父类的,BootstrapClassLoader这个东西,我一直没怎么提及过它,因为它并不是java类,而是由c/c++代码所实现的加载类的一个类加载器,到最后,我会好好说一些各个类加载器的关系,这里我们暂时先放放)。

 public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }

既然它没有父类加载器,那我们就来瞧瞧它是如何确定加载类的路径的,通过源码我们可以知道,它通过调用**getExtURLs(var1)**返回的参数来作为路径参数。那我们就去瞅瞅这个方法的源码:

  private static File[] getExtDirs() {
            String var0 = System.getProperty("java.ext.dirs");
            File[] var1;
            if (var0 != null) {
                StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
                int var3 = var2.countTokens();
                var1 = new File[var3];

                for(int var4 = 0; var4 < var3; ++var4) {
                    var1[var4] = new File(var2.nextToken());
                }
            } else {
                var1 = new File[0];
            }

            return var1;
        }

它跟AppClassLoader类获取的类加载路径的方式是一样的,也是通过java维护的系统变量中存储的值来确定类加载路径的,这个路径也同样对应着jdk环境变量的配置的路径,也就是JAVA_HOME和JRE_HOME中的配置的路径。与AppClassLoader不同的是,它既没有重写loadClass方法也没有重写findClass方法,也就是说,类加载功能是通过调用它的父类中的这两个方法来实现的。

最后我拿我自己配置的环境变量和一段代码,让大伙更加深刻理解环境变量和这两个java维护的系统变量的关系。我先贴上我的环境变量
在这里插入图片描述
在这里插入图片描述
然后贴上一段代码:

public class test {
    public static void main(String[] args) {
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println(System.getProperty("java.class.path"));
    }
}

打印结果:
在这里插入图片描述
由于java.class.path中的路径有点多,所以无法全部截出来,大家也可以在自己的电脑上去运行一下试试,然后仔细体会环境变量和这两个java维护的系统变量的关系。

总结一下:

ExtClassLoader主要负责来加载java.ext.dir系统属性中存储的文件路径中的class文件的,AppClassLoader主要负责来加载java.class.path系统属性下的文件路径中的class文件的。而这两个系统变量的内容由我们最开始提及过的jdk环境变量中的路径的配置有关系。同时AppClassLoader是作为线程上下文类加载器所设计出来的,而ExtClassLoader则是用于加载一些jdk提供给我们的工具类所使用的。

BootstrapClassLoader

同样引用《深入理解java虚拟机》一书的话来对BootstrapClassLoader进行一个解释。

BootstrapClassLoader负责加载存在在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是java虚拟机能够识别的,(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。启动类加载器无法被java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可。

通过上面的这个解释,我们就不难理解ExtClassLoader为什么把ClassLoader参数赋值为null,也就是说ExtClassLoader它的父类加载器是BootstrapClassLoader,那他们之间的关系如下:
在这里插入图片描述

而为什么没有把BootstrapClassLoader画上去是因为它并不是一个类,我们只需要理解到其中的关系就行了。

Launcher

这个类,大家平时开发的时候一般接触不到,因为这个类是java的入口,在启动java应用的时候会首先创建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);
        String var2 = System.getProperty("java.security.manager");
        if (var2 != null) {
            SecurityManager var3 = null;
            if (!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                } catch (InstantiationException var6) {
                } catch (ClassNotFoundException var7) {
                } catch (ClassCastException var8) {
                }
            } else {
                var3 = new SecurityManager();
            }

            if (var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }

在上面的这一大段的代码中我们只需要重点关注下面这两行代码,它先是通过Launcher.AppClassLoader.getAppClassLoader(var1);通过调用Launcher中的静态内部类AppClassLoader中的getAppClassLoader方法来获得一个AppClassLoader对象,而这个AppClassLoader对象是直接继承了 URLClassLoader间接继承了ClassLoader类的一个类加载器。将这个对象设置成我们上面讲解双亲委派机制的时候说的上下文线程类加载器。

  this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
  Thread.currentThread().setContextClassLoader(this.loader);

对于Launcher类的构造器已经了解的差不多了。

自定义类加载器

上面我们已经对类加载器已经了解的差不多了,现在我们来尝试着自己写一个类加载。我这边写一个重写findClass方法的类加载器,也就是符合双亲委派模型的一个类加载器。

import java.io.*;

/**
 * @ClassName test.MyClassLoader
 * @Description TODO
 * @Author Disaster
 * @Date 2021/8/19 19:16
 * @Version 1.0
 **/
public class MyClassLoader extends ClassLoader{
    private String filePath;

    public MyClassLoader(String filePath) {
        this.filePath = filePath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = getFileName(name);
        File file = new File(filePath, fileName);
        try {
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            while ((len=fis.read())!=-1){
                bos.write(len);
            }
            byte[] bytes = bos.toByteArray();
            fis.close();
            bos.close();
            return defineClass(name,bytes,0,bytes.length);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }
    protected String getFileName(String name) {
        return name + ".class";
    }
}

我把要加载的路径和.class文件贴在下面:
在这里插入图片描述
其中这个类中的内容如下图所示:
在这里插入图片描述

最后就是通过测试类来测试一下看能不能加载到.class文件。上图:
在这里插入图片描述
在这里插入图片描述
好了,自己写的类加载也实现了,如果想重写loaderclass方法也是一样能够实现类加载的功能,但是这个类加载不符合双亲委派模型,不推荐在开发中使用。不过感兴趣的还是可以自己去实现一遍,可以加深大家的理解!

总结

有个总结的习惯还是好的,就像你们有点赞的好习惯一样。类加载器其实要把它将通透非常的难,因为它涉及到的东西很多,尤其是到源码阶段的讲解更是很难,以为源码中会调用很多我们平时开发中我们接触不到的一些类,而博主本人也是从一开始的不会然后到觉得自己对类加载有了一定的认识之后写下了这篇博客,很多小伙伴如果有不懂的地方可以私信我或者在评论区一起沟通交流,最后祝各位看这篇文章的读者每天能开开心心。!

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值