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©;
}
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面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》
【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享
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());
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