写点什么

深入理解 Java 类加载器 (一):Java 类加载原理解析

  • 2021 年 11 月 12 日
  • 本文字数:4499 字

    阅读完需:约 15 分钟

if (c == null) {


// 如果父类加载器不能完成加载请求时,再调用自身的 findClass 方法进行类加载,


// 若加载成功,findClass 方法返回的是 defineClass 方法的返回值


// 注意,若自身也加载不了,会产生 ClassNotFoundException 异常并向上抛出


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;


}


}


通过上面的代码分析,我们可以对 JVM 采用的双亲委派类加载机制有了更感性的认识,下面我们就接着分析一下启动类加载器、标准扩展类加载器和系统类加载器三者之间的关系。可能大家已经从各种资料上面看到了如下类似的一幅图片:



上面图片给人的直观印象是:系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,下面我们就用代码具体测试一下:


public class Loadertest


{


public static void main(String[] args)


{


System.out.println(ClassLoader.getSystemClassLoader());


System.out.println(ClassLoader.getSystemClassLoader().getParent());


System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());


}


}


输出的结果是:



通过以上的代码输出,我们知道:通过 java.lang.ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器 ,并且可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时却得到了 null。事实上,由于启动类加载器无法被 Java 程序直接引用,因此 JVM 默认直接使用 null 代表启动类加载器。


【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


我们还是借助于代码分析一下,首先看一下 java.lang.ClassLoader 抽象类中默认实现的两个构造函数:


protected ClassLoader() {


this(checkCreateClassLoader(), getSystemClassLoader());


}


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;


}


}


紧接着,我们再看一下 ClassLoader 抽象类中 parent 成员的声明:


// The parent class loader for delegation


private ClassLoader parent;


声明为私有变量的同时并没有对外提供可供派生类访问的 public 或者 protected 设置器接口(对应的 setter 方法),结合前面的测试代码的输出,我们可以推断出:


1.系统类加载器(AppClassLoader)调用 ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用 getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)


2.扩展类加载器(ExtClassLoader)调用 ClassLoader(ClassLoader parent)构造函数将父类加载器设置为 null(null 本身就代表着引导类加载器)。(因为如果不强制设置,默认会通过调用 getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)


事实上,这就是启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系。


3、类加载双亲委派示例




以上已经简要介绍了虚拟机默认使用的启动类加载器、标准扩展类加载器和系统类加载器,并以三者为例结合 JDK 代码对 JVM 默认使用的双亲委派类加载机制做了分析。下面我们就来看一个综合的例子,首先在 IDE 中建立一个简单的 java 应用工程,然后写一个简单的 JavaBean 如下:


package com.huawei.classload001;


public class TestBean {


public TestBean() {


}


}


在现有当前工程中另外建立一个测试类(ClassLoaderTest.java)内容如下:


测试一:


package com.huawei.classload001;


public class ClassLoaderTest {


public static void main(String[] args) {


try {


//查看当前系统类路径中包含的路径条目


System.out.println(System.getProperty("java.class.path"));


//调用加载当前类的类加载器(这里即为系统类加载器)加载 TestBean


Class typeClassLoad = Class.forName("com.huawei.classload001.TestBean");


//查看被加载的 TestBean 类型是被哪个类加载器加载的


System.out.println(typeClassLoad.getClassLoader());


}


catch (ClassNotFoundException e) {


e.printStackTrace();


}


}


}


打印结果:


“C:\Program Files\Java\jdk1.8.0_91\jre\lib\charsets.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\deploy.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\access-bridge-64.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\cldrdata.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\dnsns.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jaccess.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jfxrt.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\localedata.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\nashorn.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunec.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunjce_provider.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunmscapi.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunpkcs11.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\zipfs.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\javaws.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\jce.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfr.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfxswt.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\jsse.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\management-agent.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\plugin.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\resources.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar;


D:\masterSpring\jvm\target\classes;


D:\repo\sping\org\apache\commons\commons-lang\2.6\commons-lang-2.6.jar” com.huawei.classload001.ClassLoaderTest


C:\Program Files\Java\jdk1.8.0_91\jre\lib\charsets.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\deploy.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\access-bridge-64.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\cldrdata.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\dnsns.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jaccess.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jfxrt.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\localedata.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\nashorn.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunec.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunjce_provider.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunmscapi.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunpkcs11.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\zipfs.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\javaws.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\jce.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfr.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfxswt.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\jsse.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\management-agent.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\plugin.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\resources.jar;


C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar;


D:\masterSpring\jvm\target\classes;


D:\repo\sping\org\apache\commons\commons-lang\2.6\commons-lang-2.6.jar;


D:\idea\IntelliJ IDEA 2018.3.1\lib\idea_rt.jar


sun.misc.Launcher$AppClassLoader@18b4aac2


测试二:


将当前工程输出目录下的 TestBean.class 打包进 test.jar 剪贴到/lib/ext 目录下(现在工程输出目录下和 JRE 扩展目录下都有待加载类型的 class 文件)。再运行测试一测试代码,结果如下:



对比测试一和测试二,我们明显可以验证前面说的双亲委派机制:系统类加载器在接到加载 classloader.test.bean.TestBean 类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。


测试三:


将 test.jar 拷贝一份到/lib 下,运行测试代码,输出如下:


I:\AlgorithmPractice\TestClassLoader\bin


sun.misc.Launcher$ExtClassLoader@15db9742


测试三和测试二输出结果一致。那就是说,放置到/lib 目录下的 TestBean 对应的 class 字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载<JAVA_HOME>/lib 目录下存在的陌生类,换句话说,虚拟机只加载<JAVA_HOME>/lib 目录下它可以识别的类。因此,开发者通过将要加载的非 JDK 自身的类放置到此目录下期待启动类加载器加载是不可能的。做个进一步验证,删除<JAVA_HOME>/lib/ext 目录下和工程输出目录下的 TestBean 对应的 class 文件,然后再运行测试代码,则将会有 ClassNotFoundException 异常抛出。有关这个问题,大家可以在 java.lang.ClassLoader 中的 loadClass(String name, boolean resolve)方法中设置相应断点进行调试,会发现 findBootstrapClass0()会抛出异常,然后在下面的 findClass 方法中被加载,当前运行的类加载器正是扩展类加载器(sun.misc.Launcher$ExtClassLoader),这一点可以通过 JDT 中变量视图查看验证。


三. Java 程序动态扩展方式


=================================================================================


Java 的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器,你的程序可以加载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。运行时动态扩展 java 应用程序有如下两个途径:


1、反射(调用 java.lang.Class.forName(…)加载类)


这个方法其实在前面已经讨论过,在后面的问题 2 解答中说明了该方法调用会触发哪个类加载器开始加载任务。这里需要说明的是多参数版本的 forName(…)方法:


public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException

评论

发布
暂无评论
深入理解Java类加载器(一):Java类加载原理解析