Skip to content

类加载器 ​

内容来源:本篇为学习整理,转载自网络技术博客(版权归原作者所有):

看在前面 ​

Question ​

  • 类加载过程

  • ClassLoader理解、原理?

  • 双亲委派模型理解?

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

  • 怎么样打破双亲委派模型?

  • 自定义类加载器实现过程?

  • JVM在搜索类的时候,又是如何判定两个class是相同的呢?

  • 线程上下文类加载器

一、类加载过程 ​

Class 文件需要加载到虚拟机中之后才能运行和使用。虚拟机加载这些 Class 文件的过程:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。

类加载过程

一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段。这一步我们可以去完成。还可以自定义类加载器,通过重写一个类加载器的 loadClass() 方法控制字节流的获取方式。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。所有的类都由类加载器加载,加载的作用就是将 .class文件加载到内存。

可以类比到web服务接收客户端上传特定类型文件(csv文件)初始化过程,便于理解记忆。

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期共经历 7 个阶段

  • 加载(Loading)

  • 验证(Verification)

  • 准备(Preparation)

  • 解析(Resolution)

  • 初始化(Initialization)

  • 使用(Using)

  • 卸载(Unloading)

「解析阶段」在某些情况下可以在「初始化阶段」之后再开始,这是为了支持 Java 语言的运行时绑定特性。

涉及名词解释:

  • 符号引用:符号引用以一组符号来描述所引用的目标。符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。 符号引用与虚拟机实现的内存布局无关。引用的目标并不一定是已经加载到虚拟机内存中的内容。 各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的。原因是符号引用的字面量形式明确定义在《Java 虚拟机规范》的 Class 文件格式中。

  • 直接引用:直接引用是可以直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。 直接引用是和虚拟机实现的内存布局直接相关的。同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

加载 ​

类加载过程的第一步,主要完成下面3件事情:

  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

虚拟机规范多上面这3点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取、怎样获取。比如:常见的就是从 ZIP 包读取(日后出现的JAR、EAR、WAR格式的基础)、其他文件生成(典型的是JSP)这类方式。

一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段。这一步我们可以去完成。还可以自定义类加载器,通过重写一个类加载器的 loadClass() 方法控制字节流的获取方式。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。

类加载器、双亲委派模型也是非常重要的知识点,这部分内容会在后面的文章中单独介绍到。加载阶段和连接阶段的部分内容交叉执行,加载阶段尚未结束,连接阶段可能就已经开始了。

验证 ​

验证阶段示意图

验证的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求。这样才能保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

  • 文件格式验证: 验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。

  • 元数据验证: 对字节码描述的信息做语义分析,以保证其描述的信息符合《Java 语言规范》的要求。

  • 字节码验证: 通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。

  • 符号引用验证: 简单来说就是该类是否缺少或者被禁止访问它依赖的某些资源。例如外部类、方法、字段。此验证行为发生在虚拟机将符号引用转化为直接引用的时候,即「解析阶段」。

准备 ​

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1. 这时候分配内存的仅包括类变量(static),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  2. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等)。比如我们定义了public static int value = 111 ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 fianl 关键字public static final int value = 111 。那么准备阶段 value 的值就被赋值为 111。

基本数据类型的零值:

基本数据类型的零值

解析 ​

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对 7 类符号引用:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用限定符。

符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的。举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。

Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法时,只要知道这个方法在方法表中的偏移量,就可以直接调用该方法。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置。从而使得方法可以被调用。

综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。也就是得到类或者字段、方法在内存中的指针或者偏移量。

初始化 ​

初始化是类加载的最后一步。这一步真正执行类中定义的 Java 程序代码(字节码)。初始化阶段是执行类构造器 <clinit>()方法的过程。对于<clinit>() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 <clinit>() 方法是带锁线程安全,所以在多线程环境下初始化类可能会引起死锁。并且这种死锁很难被发现。

对于初始化阶段,虚拟机严格规范了有且只有 5 种情况必须对类初始化:

  1. 当遇到 new 、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时。比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法。
  2. 使用 java.lang.reflect 包的方法对类反射调用时,如果类没初始化,触发其初始化。
  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  4. 虚拟机启动时,会先初始化用户定义的主类 (包含 main 方法的那个类)。
  5. 使用 JDK1.7 的动态语言时,如果 MethodHandle 实例的最后解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄。这个句柄没有初始化时,需要先触发其初始化。

二、类加载器总结 ​

2.1 回顾什么是ClassLoader ​

当我们写好一个Java程序之后,不管是CS还是BS应用,都是由若干个.class文件组织而成。这些文件组成一个完整的Java应用程序。当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能。而这些功能都被封装在不同的class文件当中。所以经常要从一个class文件中调用另外一个class文件中的方法。如果另外一个文件不存在,则会引发系统异常。

而程序在启动的时候,并不会一次性加载程序所要用的所有class文件。程序根据需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中。只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

2.2 ClassLoader原理介绍 ​

ClassLoader使用的是双亲委托模型来搜索类的。每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系)。虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的父类加载器。

当一个ClassLoader实例需要加载某个类时,它会先把这个任务委托给父类加载器,之后才自己搜索这个类。这个过程是由上至下依次检查的。

首先由最顶层的类加载器Bootstrap ClassLoader试图加载。如果没加载到,则把任务转交给Extension ClassLoader试图加载。如果也没加载到,则转交给App ClassLoader加载。如果它也没有加载到,则返回给委托的发起者。由它到指定的文件系统或网络等URL中加载该类。

如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中。最后返回这个类在内存中的Class实例对象。

2.3 类加载器总结 ​

JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader:

  1. BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现。负责加载 %JAVA_HOME%/lib目录下的jar包和类。也加载被 -Xbootclasspath参数指定的路径中的所有类。

  2. ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类。也加载被 java.ext.dirs 系统变量指定的路径下的jar包。

  3. AppClassLoader(应用程序类加载器) :面向用户的加载器。负责加载当前应用 classpath 下的所有jar包和类。

三、双亲委派模型 ​

3.1 双亲委派模型介绍 ​

每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 loadClass() 处理。因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。

当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。

每个类加载都有一个父类加载器,我们通过下面的程序来验证。

java
public class ClassLoaderDemo {
    public static void main(String[] args) {
        System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
        System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());
        System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
    }
}

Output

java
ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@1b6d3586
The GrandParent of ClassLodarDemo's ClassLoader is null

AppClassLoader的父类加载器为ExtClassLoader, ExtClassLoader的父类加载器为null,null并不代表ExtClassLoader没有父类加载器,而是 BootstrapClassLoader 。

其实这个双亲翻译的容易让别人误解,我们一般理解的双亲都是父母,这里的双亲更多地表达的是“父母这一辈”的人而已,并不是说真的有一个 Mother ClassLoader 和一个 Father ClassLoader 。另外,类加载器之间的“父子”关系也不是通过继承来体现的,是由“优先级”来决定。官方API文档对这部分的描述如下:

The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a "parent" class loader. When loading a class, a class loader first "delegates" the search for the class to its parent class loader before attempting to find the class itself.

3.2 双亲委派模型实现源码分析 ​

双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader 的 loadClass() 中,相关代码如下所示。

java
private final ClassLoader parent; 
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) {//父加载器不为空,调用父加载器loadClass()方法处理
                        c = parent.loadClass(name, false);
                    } else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //抛出异常说明父类加载器无法完成加载请求
                }
                
                if (c == null) {
                    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;
        }
    }

补充双亲委派机制内容 ​

JDK 9 前的双亲委派模式

JDK 9 之前的 Java 应用都是由「启动类加载器」、「扩展类加载器」、「应用程序类加载器」这三种类加载器互相配合来完成加载的,如果有需要还可以加入自定义的类加载器来进行拓展。

自定义的类加载器场景:典型的如增加除了磁盘位置之外的 Class 文件来源,或者通过类加载器实现类的隔离、重载等功能。

  • 工作过程:

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

  • 为什么要使用这个模式?

Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系。

例如:类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都能够保证是同一个类。

JDK 9 的双亲委派模式

JDK 9 为了模块化的支持,对双亲委派模式做了一些改动:

1). 扩展类加载器被平台类加载器(Platform ClassLoader)取代。

JDK 9 时基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数十个 JMOD 文件), 其中的 Java 类库就已天然地满足了可扩展的需求,那自然无须再保留 <JAVA_HOME>\lib\ext 目录,此前使用这个目录或者 java.ext.dirs 系统变量来扩展 JDK 功能的机制已经没有继续存在的价值了。

2).平台类加载器和应用程序类加载器都不再继承自 java.net.URLClassLoader。

现在启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader。

如果有程序直接依赖了这种继承关系,或者依赖了 URLClassLoader 类的特定方法,那代码很可能会在 JDK 9 及更高版本的 JDK 中崩溃。

3).启动类加载器现在是在 Java 虚拟机内部和 Java 类库共同协作实现的类加载器(以前是 C++实现)。

为了与之前的代码保持兼容,所有在获取启动类加载器的场景(譬如 Object.class.getClassLoader)中仍然会返回 null 来代替,而不会得到 BootClassLoader 的实例。

4). 类加载的委派关系也发生了变动。

当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。

在 Java 模块化系统明确规定了三个类加载器负责各自加载的模块:

  • 启动类加载器负责加载的模块
java
java.base                        java.security.sasl
java.datatransfer                java.xml
java.desktop                     jdk.httpserver
java.instrument                  jdk.internal.vm.ci
java.logging                     jdk.management
java.management                  jdk.management.agent
java.management.rmi              jdk.naming.rmi
java.naming                      jdk.net
java.prefs                       jdk.sctp
java.rmi                         jdk.unsupported
  • 平台类加载器负责加载的模块
java
java.activation*                jdk.accessibility
java.compiler*                  jdk.charsets
java.corba*                     jdk.crypto.cryptoki
java.scripting                  jdk.crypto.ec
java.se                         jdk.dynalink
java.se.ee                      jdk.incubator.httpclient
java.security.jgss              jdk.internal.vm.compiler*
java.smartcardio                jdk.jsobject
java.sql                        jdk.localedata
java.sql.rowset                 jdk.naming.dns
java.transaction*               jdk.scripting.nashorn
java.xml.bind*                  jdk.security.auth
java.xml.crypto                 jdk.security.jgss
java.xml.ws*                    jdk.xml.dom
java.xml.ws.annotation*         jdk.zipfs
  • 应用程序类加载器负责加载的模块
java
jdk.aot                         jdk.jdeps
jdk.attach                      jdk.jdi
jdk.compiler                    jdk.jdwp.agent
jdk.editpad                     jdk.jlink
jdk.hotspot.agent               jdk.jshell
jdk.internal.ed                 jdk.jstatd
jdk.internal.jvmstat            jdk.pack
jdk.internal.le                 jdk.policytool
jdk.internal.opt                jdk.rmic
jdk.jartool                     jdk.scripting.nashorn.shell
jdk.javadoc                     jdk.xml.bind*
jdk.jcmd                        jdk.xml.ws*
jdk.jconsole

双亲委派模式示意图

双亲委派模式示意图

覆盖机制 java.endorsed.dirs 变化

JDK 9 之前可设置系统属性 <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/standards/index.html">java.endorsed.dirs</a> 指定覆盖 JDK 的中的组件,或者直接将覆盖类打包为 JAR 文件放入 <java-home>\lib\endorsed 目录

JDK 9 因为模块化的设计删除了<a href="https://docs.oracle.com/en/java/javase/13/migrate/index.html#JSMIG-GUID-8E83E51A-88A3-4E9A-8E2A-66E1D66A966C">该机制</a> ,可以使用可升级模块或将 JAR 文件放在类路径中。

3.3 双亲委派模型的好处 or 为什么要使用双亲委托这种模型呢? ​

3.3.1 双亲委派模型的好处 ​

双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

3.3.2 为什么要使用双亲委托这种模型呢? ​

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

3.4 如何破坏双亲委派模型呢? ​

第一种,自定义类加载器,必须重写findClass和loadClass;

第二种是通过线程上下文类加载器的传递性,让父类加载器中调用子类加载器的加载动作。

四、自定义类加载器 ​

除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自 java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader。

关于自定义类加载器可以参照:<a href="https://blog.csdn.net/u013412772/article/details/80848909">Java类加载器--自定义类加载器(ClassLoader)</a>

JVM在搜索类的时候,又是如何判定两个class是相同的呢?

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。现在通过实例来验证上述所描述的是否正确:

1. 在web服务器上建一个org.classloader.simple.NetClassLoaderSimple.java类

java
package org.classloader.simple;
 
public class NetClassLoaderSimple {
	
	private NetClassLoaderSimple instance;
 
	public void setNetClassLoaderSimple(Object obj) {
		this.instance = (NetClassLoaderSimple)obj;
	}
}

org.classloader.simple.NetClassLoaderSimple类的setNetClassLoaderSimple方法接收一个Object类型参数,并将它强制转换成org.classloader.simple.NetClassLoaderSimple类型。

2. 自定义类加载器:NetWorkClassLoader

java
package classloader;
 
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
 
/**
 * 加载网络class的ClassLoader
 */
public class NetworkClassLoader extends ClassLoader {
	
	private String rootUrl;
 
	public NetworkClassLoader(String rootUrl) {
		this.rootUrl = rootUrl;
	}
 
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		Class clazz = null;//this.findLoadedClass(name); // 父类已加载	
		//if (clazz == null) {	//检查该类是否已被加载过
			byte[] classData = getClassData(name);	//根据类的二进制名称,获得该class文件的字节码数组
			if (classData == null) {
				throw new ClassNotFoundException();
			}
			clazz = defineClass(name, classData, 0, classData.length);	//将class的字节码数组转换成Class类的实例
		//} 
		return clazz;
	}
 
	private byte[] getClassData(String name) {
		InputStream is = null;
		try {
			String path = classNameToPath(name);
			URL url = new URL(path);
			byte[] buff = new byte[1024*4];
			int len = -1;
			is = url.openStream();
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			while((len = is.read(buff)) != -1) {
				baos.write(buff,0,len);
			}
			return baos.toByteArray();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (is != null) {
			   try {
			      is.close();
			   } catch(IOException e) {
			      e.printStackTrace();
			   }
			}
		}
		return null;
	}
 
	private String classNameToPath(String name) {
		return rootUrl + "/" + name.replace(".", "/") + ".class";
	}
 
}

3. 测试两个class是否相同

java
package classloader;
 
public class NewworkClassLoaderTest {
 
	public static void main(String[] args) {
		try {
			//测试加载网络中的class文件
			String rootUrl = "http://localhost:8080/httpweb/classes";
			String className = "org.classloader.simple.NetClassLoaderSimple";
			NetworkClassLoader ncl1 = new NetworkClassLoader(rootUrl);
			NetworkClassLoader ncl2 = new NetworkClassLoader(rootUrl);
			Class<?> clazz1 = ncl1.loadClass(className);
			Class<?> clazz2 = ncl2.loadClass(className);
			Object obj1 = clazz1.newInstance();
			Object obj2 = clazz2.newInstance();
			clazz1.getMethod("setNetClassLoaderSimple", Object.class).invoke(obj1, obj2);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
}

首先获得网络上一个class文件的二进制名称,然后通过自定义的类加载器NetworkClassLoader创建两个实例,并根据网络地址分别加载这份class,并得到这两个ClassLoader实例加载后生成的Class实例clazz1和clazz2,最后将这两个Class实例分别生成具体的实例对象obj1和obj2,再通过反射调用clazz1中的setNetClassLoaderSimple方法。其中两个ClassLoader实例指的是new了两个不同的NetworkClassLoader实例对象。

4. 测试结果

自定义ClassLoader加载class文件

结论:从结果中可以看出,虽然是同一份class字节码文件,但是由于被两个不同的ClassLoader实例所加载,所以JVM认为它们就是两个不同的类。

五、线程上下文类加载器 ​

推荐:<a href="https://blog.csdn.net/wangdongli_1993/article/details/81976992?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-4-81976992-blog-52631940.235^v38^pc_relevant_yljh&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-4-81976992-blog-52631940.235^v38^pc_relevant_yljh&utm_relevant_index=5">深入理解线程上下文类加载器</a>

java默认的线程上下文类加载器是系统类加载器(AppClassLoader)。其中sun.misc.Launcher类中无参构造方法Launcher()如下所示:

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

}

使用线程上下文类加载器,可以在执行线程中,抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。典型的例子有,通过线程上下文来加载第三方库jndi实现,而不依赖于双亲委派。

大部分java app服务器(jboss, tomcat...)也是采用contextClassLoader来处理web服务。还有一些采用 hotswap 特性的框架,也使用了线程上下文类加载器,比如 seasar (full stack framework in japenese)。线程上下文从根本解决了一般应用不能违背双亲委派模式的问题。使java类加载体系显得更灵活。

随着多核时代的来临,相信多线程开发将会越来越多地进入程序员的实际编码过程中。因此,在编写基础设施时, 通过使用线程上下文来加载类,应该是一个很好的选择。当然,好东西都有利弊。使用线程上下文加载类,也要注意,保证多个需要通信的线程间的类加载器应该是同一个, 防止因为不同的类加载器,导致类型转换异常(ClassCastException)。