一张图概括下类加载机制和类加载器:

JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。

类加载器的作用

类的加载是需要类加载器完成的,但是类加载器在JVM中的作用可不止这些。在JVM中,一个类的唯一性是需要这个类本身和类加载器一起才能确定的,每个类加载器都有一个独立的命名空间。

Java中的类加载器

启动类加载器:Bootstrap ClassLoader

这个类加载器使用C/C++语言实现的,嵌套在JVM内部,java程序无法直接操作这个类。

它用来加载Java核心类库,如:JAVA_HOME/jre/lib/rt.jar、resources.jar、sun.boot.class.path路径下的包,用于提供jvm运行所需的包。

并不是继承自java.lang.ClassLoader,它没有父类加载器

它加载扩展类加载器和应用程序类加载器,并成为他们的父类加载器

出于安全考虑,启动类只加载包名为:java、javax、sun开头的类

扩展类加载器:Extension ClassLoader

Java语言编写,由sun.misc.Launcher$ExtClassLoader实现,我们可以用Java程序操作这个加载器。派生继承自java.lang.ClassLoader,父类加载器为启动类加载器

从系统属性:java.ext.dirs目录中加载类库,或者从JDK安装目录:jre/lib/ext目录下加载类库。我们就可以将我们自己的包放在以上目录下,就会自动加载进来了。

应用程序类加载器:Application Classloader

Java语言编写,由sun.misc.Launcher$AppClassLoader实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。派生继承自java.lang.ClassLoader,父类加载器为启动类加载器

它负责加载环境变量classpath或者系统属性java.class.path指定路径下的类库。

如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器,我们Java程序中的类,都是由它加载完成的。

我们可以通过ClassLoader#getSystemClassLoader()获取并操作这个加载器。

自定义加载器

一般情况下,以上3种加载器能满足我们日常的开发工作,不满足时,我们还可以自定义加载器。比如用网络加载Java类,为了保证传输中的安全性,采用了加密操作,那么以上3种加载器就无法加载这个类,这时候就需要自定义加载器

自定义加载器实现步骤:

继承java.lang.ClassLoader类,重写findClass()方法。如果没有太复杂的需求,可以直接继承URLClassLoader类,重写loadClass方法,具体可参考AppClassLoader和ExtClassLoader。

类加载器关系如图所示:

双亲委派模型是什么?

说完了类加载器,下面我们就说一下什么是双亲委派模型吧。

jvm对class文件采用的是按需加载的方式,当需要使用该类时,jvm才会将它的class文件加载到内存中产生class对象。

在加载类的时候,是采用的双亲委派机制,即把请求交给父类处理的一种任务委派模式。

工作原理

(1)如果一个类加载器接收到了类加载的请求,它自己不会先去加载,会把这个请求委托给父类加载器去执行。

(2)如果父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader 。

(3)如果父类加载器可以完成加载任务,就返回成功结果,如果父类加载失败,就由子类自己去尝试加载,如果最后的子类加载失败就会抛出ClassNotFoundException异常,这就是双亲委派模式。

【举个例子】

双亲委派整个过程分为以下几步:

  • 假设用户刚刚摸鱼写的Test类想进行加载,这个时候首先会发送给应用程序类加载器AppCloassLoader;

  • 然后AppClassLoader并不会直接去加载Test类,而是会委派于父类加载器完成此操作,也就是ExtClassLoader;

  • ExtClassLoader同样也不会直接去加载Test类,而是会继续委派于父类加载器完成,也就是BootstrapClassLoader;

  • BootstrapClassLoader这个时候已经到顶层了,没有父类加载器了,所以BootstrapClassLoader会在jdk/lib目录下去搜索是否存在,因为这里是用户自己写的Test类,是不会存在于jdk下的,所以这个时候会给子类加载器一个反馈。

  • ExtClassLoader收到父类加载器发送的反馈,知道了父类加载器并没有找到对应的类,爸爸靠不住,就只能自己来加载了,结果显而易见,自己也不行,没办法,只能给更下面的子类加载器了。

  • AppClassLoader收到父类加载器的反馈,顿时明白,原来爸爸虽然是爸爸,但是他终究不能管儿子的私事,所以这时候,AppClassLoader就自己尝试去加载。

  • 结果,就这样成功了,走了一大圈,兜兜转转还是自己干。

为什么要使用双亲委派模型?

如上面我们提到的,因为类加载器之间有严格的层次关系,那么也就使得Java类也随之具备了层次关系。或者说这种层次关系是优先级。

这种机制有几个好处:

  • 首先,通过委派的方式,可以 避免类的重复加载 ,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。

  • 另外,通过双亲委派的方式,还 保证了安全性 。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.String,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。

【举个例子】

以前,爱捣鼓的小明突然灵机一动,写出了下面的代码:

package java.lang;

public class String { 
	//...复制真正String的其他方法 :
	//public boolean equals(Object anObject) { sendEmail(xxx); return equalsReal(anObject); } 
	//...
}

这样,只要引用java.lang.String的人,小明能随时收到他的系统的相关信息,这简直是个天才的注意。然而实施的时候却发现,JVM并没有加载这个类。

这是为什么呢?

虽然小明自定义了String,包名也叫java.lang,但是当用户使用String的时候,会先请求普通的Application ClassLoader加载java.lang.String,此时通过双亲委派,类加载请求会上传给Application ClassLoader的父类,直到传递给Bootstrap ClassLoader,而此时,Bootstrap ClassLoader将在%JAVA_HOME%/lib中寻找java.lang.String而此时正好能够找到java.lang.String,加载成功,返回。因此小明自己写的java.lang.String并没有被加载。

可以看见,如果真的想要实现小明的计划,只能将小明自己编写的java.lang.String这个class文件替换到%JAVA_HOME%/lib/rt.jar 中的String.class

"父子加载器"之间的关系是继承吗?

很多人看到父加载器、子加载器这样的名字,就会认为Java中的类加载器之间存在着继承关系。这里需要明确一下,双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用 组合(Composition)关系 来复用父加载器的代码的。

如下为ClassLoader中父加载器的定义:

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;
}

双亲委派是怎么实现的?

双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现并不复杂。

ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。

实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中:

public abstract class ClassLoader {
    // 委派的父类加载器
    private final ClassLoader parent;

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查该类是否被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                    	//父类加载器不为空,则用该父类加载器
                        c = parent.loadClass(name, false);
                    } else {
                    	//若父类加载器为空,则使用启动类加载器作为父类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    //若父类加载器抛出ClassNotFoundException ,
                    //则说明父类加载器无法完成加载请求
                }

                if (c == null) {
    				//父类加载器无法完成加载请求时
    				//调用自身的findClass()方法进行类加载
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

代码不难理解,主要就是以下几个步骤:

1、先检查类是否已经被加载过 ;

2、若没有加载则调用父加载器的loadClass()方法进行加载 ;

3、若父加载器为空则默认使用启动类加载器作为父加载器;

4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

loadClass()、findClass()、defineClass()区别

ClassLoader中和类加载有关的方法有很多,前面提到了loadClass,除此之外,还有findClass和defineClass等,那么这几个方法有什么区别呢?

  • loadClass():就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。

  • findClass():根据名称或位置加载.class字节码。

  • definclass():把.class字节码转化为Class对象 。

loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 是否已经加载了目标类,如果没有找到就会让父加载器尝试加载,如果父加载器都加载不了,就会抛出ClassNotFoundException异常后,调用 findClass() 让自己来加载目标类。不同的加载器将使用不同的findClass()逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。

如何打破双亲委派模型?

知道了双亲委派模型的实现,那么想要破坏双亲委派机制就很简单了。

因为他的双亲委派过程都是在loadClass方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器继承ClassLoader,重写其中的 loadClass() 方法,使其不进行双亲委派即可。

还有一种破坏方法:使用线程上下文类加载器。

自定义类加载器

通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。

我们前面说过,当我们想要自定义一个类加载器的时候,并且想破坏双亲委派原则时,我们会重写loadClass方法。那么,如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?

这时候,就可以继承ClassLoader,并且重写 findClass() 方法。findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法。

JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中。因为在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载。

所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass中实现你自己的加载逻辑即可。

参考:

https://juejin.cn/post/6844903729435508750

https://juejin.cn/post/6916314841472991239

https://www.huaweicloud.com/articles/d73655210827a50c89f6b5645d8182a6.html

https://juejin.cn/post/6865572557329072141

https://segmentfault.com/a/1190000037574626