JVMの奇妙な冒険——第一话:JVM类加载机制
JVMの奇妙な冒険——第一话:JVM类加载机制
学习目标
- 理解Java类加载运行的全过程
- 通过JDK源码了解JVM核心类加载器
- 了解类加载双亲委派机制
- 手写自定义类加载器打破双亲委派机制
扩展:
Tomcat类加载机制以及手写Tomcat类加载器实现多代码共存隔离
JVM类加载机制
深入理解Java虚拟机第七章第一节概述说:
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称为虚拟机的类加载机制。
那么,虚拟机的类加载机制具体怎样的一个过程,怎么实现呢?
类加载运行全过程
当我们用Java命令运行某个类的main函数启动程序或者使用到某个类时,首先需要通过类加载器把主类加载到JVM。
如下面的程序:
package com.jvm01;
public class Math {
public static final int initData = 666;
public static User user = new User();
//一个方法对应一块栈帧内存区域
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
在执行代码时的大致流程如下所示:
其中,类加载过程有如下几步:
一个类型从被加载到虚拟机内存开始,到卸载出内存为止,他的整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备、解析三个部分统称为连接。
加载>>验证>>准备>>解析>>初始化>>使用>>卸载
在Java代码中,类型的加载、连接和初始化过程都是在程序运行期间完成的。
-
加载:使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段将类的.class文件中的二进制数据读入内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象(规范中并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类在方法区内的数据结构。具体来说,这期间,Java虚拟机需要完成以下三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口
-
验证:校验字节码文件的正确性(cafe babe)
-
准备:给类的静态变量分配空间,并赋予默认值(0、false、null)
-
解析:将符号引用替换为直接引用,该阶段会把一些静态方法(static修饰的方法,也就是符号引用,比方说代码里的main())替换为指向数据所在内存的指针或句柄等(也就是直接引用),这就是所谓的静态链接过程(类加载期间完成的)。
非静态方法在执行过程中可能会有不同的实现,所以不会在类加载过程中进行解析,而是要到真正使用到的时候才会将符号引用替换为直接引用(这就是所谓的动态链接过程);相比较于其他的方法,由于static修饰的静态方法在类加载之后不会再改变,所以在类加载的解析阶段先执行静态链接的过程以此提高效率。
-
初始化:对类的静态变量初始化为指定的值,执行静态代码块
符号引用(Symbolic Referen):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用:直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。直接引用可以有不同的实现方式:
- 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
- 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
- 一个能间接定位到目标的句柄
Java类的类加载是一种懒加载 ,只有要使用到的时候才会进行加载。比如在我们平时运行一个类的时候会使用到很多其他的类,在运行过程中如果主类需要使用到其它类了,会逐步加载这些类。项目中引用的jar包或war包里的类也不是一次性全部加载的,是使用到时才加载。举个例子:
public class TestDynamicLoad {
static {
System.out.println("*************load TestDynamicLoad************");
}
public static void main(String[] args) {
new A();
System.out.println("*************load test************");
//B不会加载,除非这里执行 new B()
B b = null;
System.out.println("==================================");
B.b();
}
}
class A {
static {
System.out.println("*************load A************");
}
public A() {
System.out.println("*************initial A************");
}
}
class B {
static {
System.out.println("*************load B************");
}
public B() {
System.out.println("*************initial B************");
}
public static void b(){
System.out.println("*************static b************");
}
}
我们先根据前面所学的类加载过程分析一下最终运行的结果:
首先我们执行这个主类:TestDynamicLoad,它有静态代码块和main方法
- 因为在类加载过程中的初始化阶段会执行静态代码块,所以先输出
*************load TestDynamicLoad************
- 然后再执行main方法,首先执行到new A();需要初始化A类了,所以要先加载A类。同样的道理,先执行A的静态代码块,所以输出
*************load A************
- 然后初始化A类,调用A类的构造方法,输出
*************initial A************
- 初始化A类之后继续执行主类的main方法,输出
*************load test************
- 接下来就是执行到B b = null,由于没有使用到B类比如说初始化B类或者调用B类的静态方法,所以没有输出
- 然后输出
==================================
作为一道华丽的分隔符 - 接下来执行了B类的静态方法b,因此使用到B类了,需要先加载B类,自然就先执行B类的静态代码块,输出
*************load B************
- 最后执行静态方法b,输出
*************static b************
我们看看最终的执行结果,有图有真相:
主动引用和被动引用
主动引用
对于初始化阶段,《Java虚拟机规范》严格规定了有且只有六种情况必须立即对类进行“初始化”(加载、验证、准备、解析自然需要在此之前):
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要触发其初始化阶段。场景:
- 使用new关键字实例化对象时
- 读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
- 调用一个类的静态方法的时候
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先初始化
- 初始化类时,如果其父类还没有初始化,则需要先初始化其父类
- 执行一个主类的main方法时,先初始化这个主类
- 当使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、PEF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要触发其初始化
- 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前初始化
被动引用
示例1:
package com.example.jvm.classload;
/**
* @author LinKai
* @time 2021/03/13-18:34:00
*/
public class NotInitialzation {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
class SuperClass {
public static int value = 123;
static {
System.out.println("SuperClass Init");
}
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass Init");
}
}
运行结果:
SuperClass Init
123
根据结果我们可以知道,虚拟机对SuperClass类主动引用,对SubClass类被动引用。
对于静态字段,只有直接定义了这个字段的类才会初始化;通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
我们可以添加JVM参数-XX:+TraceClassLoading
(用于追踪类的加载信息并打印出来)观察到此操作是会导致子类加载的,但是不初始化:
[Loaded com.example.jvm.classload.SuperClass from file:/D:/idea_workspace/study-code/jvm/target/classes/]
[Loaded com.example.jvm.classload.SubClass from file:/D:/idea_workspace/study-code/jvm/target/classes/]
[Loaded java.util.Properties$LineReader from C:\Program Files\Java\jdk1.8.0_271\jre\lib\rt.jar]
SuperClass Init
123
我们顺便介绍一下JVM参数:
-XX:+<option>
:表示开启option选项-XX:-<option>
:表示关闭option选项-XX:<option>=<value>
:表示将option选项的值设置为value
示例2:
package com.example.jvm.classload;
/**
* @author LinKai
* @time 2021/03/13-20:28:00
*/
public class NotInitialzation2 {
public static void main(String[] args) {
SubClass[] sb = new SubClass[2];
}
}
运行结果没有任何输出。
这是因为通过数组定义来引用类,不会触发该类的初始化。
示例3:
package com.example.jvm.classload;
/**
* @author LinKai
* @time 2021/03/13-20:40:00
*/
public class NotInitialzation3 {
public static void main(String[] args) {
System.out.println(ConstClass.TEST);
}
}
class ConstClass {
public static final String TEST = "test";
static {
System.out.println("ConstClass init");
}
}
运行结果只输出test。
但是如果把静态常量TEST改为静态变量,运行结果输出ConstClass init \n test。
因为常量在编译阶段会存入调用这个常量的方法所在的类的常量池中,本质上调用类没有直接引用到定义常量的类,所以不会触发定义常量的类的初始化。
类加载器
上面的类加载过程主要通过类加载器实现的,Java里有如下几种类加载器:
- 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,如rt.jar、charsets.jar等
- 扩展类加载器:负责加载JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
- 应用程序类加载器:负责加载ClassPath路径下的类包(主要就是加载我们编写的类)
- 自定义加载器:负责加载用户自定义路径下的类包
我们先举个例子:
package com.example.jvm.classload;
import sun.misc.Launcher;
import java.net.URL;
/**
* @author LinKai
* @time 2021/03/03-11:35:00
*/
public class TestJDKClassLoader {
public static void main(String[] args) {
System.out.println("********示例1*******");
System.out.println(String.class.getClassLoader());
//com.sun.crypto.provider.DESKeyFactory位于ext文件下的sunjce_provider.jar
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());
System.out.println(TestJDKClassLoader.class.getClassLoader());
System.out.println("********示例2*******");
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassloader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassloader.getParent();
System.out.println("应用程序类加载器: " + appClassLoader);
System.out.println("应用程序类加载器的parent扩展类加载器: " + extClassloader);
System.out.println("扩展类加载器的parent引导类加载器: " + bootstrapLoader);
System.out.println("********示例3*******");
System.out.println("bootstrapLoader加载以下文件:");
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i]);
}
System.out.println("********示例4*******");
System.out.println("extClassloader加载以下文件:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("********示例5*******");
System.out.println("appClassLoader加载以下文件:");
System.out.println(System.getProperty("java.class.path"));
}
}
示例一的结果:
null
sun.misc.Launcher$ExtClassLoader@7cca494b
sun.misc.Launcher$AppClassLoader@18b4aac2
解释:
- String类是核心类库中的类,所以加载String类的是引导类加载器,由于引导类加载器是C++写的,不是Java对象,所以打印出来的是null
- 因为类com.sun.crypto.provider.DESKeyFactory是位于ext扩展目录中的类,所以加载它的加载器是扩展类加载器,我们可以看到,它叫ExtClassLoader,是Launcher类的内部类,而前面的流程图我们可以看到Launcher类是JVM启动器实例
- 同样,AppClassLoader也是Launcher类的内部类,它加载的是我们所编写的java类,所以它是应用程序类加载器
示例2的结果:
应用程序类加载器: sun.misc.Launcher$AppClassLoader@18b4aac2
应用程序类加载器的parent扩展类加载器: sun.misc.Launcher$ExtClassLoader@7cca494b
扩展类加载器的parent引导类加载器: null
已经很明显了,我们依次获得了应用程序类加载器、应用程序类加载器的parent属性、扩展类加载器的parent属性,可以看出三种类加载器的关系,应用程序类加载器的parent属性其实就是扩展类加载器,而扩展类加载器的parent属性虽然输出结果是null,但是我们也可以知道它就是引导类加载器。
具体我们继续往后看,了解扩展类加载器和应用程序类加载器的源码就知道了。
示例3的结果我们可以看出引导类加载器都加载了我们哪些路径下的类或者哪些jar包,其实就是本地安装Java目录下的核心库类:
/**
* bootstrapLoader加载以下文件:
* file:/D:/Java/jdk1.8.0_171/jre/lib/resources.jar
* file:/D:/Java/jdk1.8.0_171/jre/lib/rt.jar
* file:/D:/Java/jdk1.8.0_171/jre/lib/sunrsasign.jar
* file:/D:/Java/jdk1.8.0_171/jre/lib/jsse.jar
* file:/D:/Java/jdk1.8.0_171/jre/lib/jce.jar
* file:/D:/Java/jdk1.8.0_171/jre/lib/charsets.jar
* file:/D:/Java/jdk1.8.0_171/jre/lib/jfr.jar
* file:/D:/Java/jdk1.8.0_171/jre/classes
*/
示例4展示的就是扩展类加载器加载的类的路径:
extClassloader加载以下文件:
D:\Java\jdk1.8.0_271\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
示例5我们可以看到包括示例3由引导类加载器加载的核心库类的jar以及扩展类加载器所加载的扩展目录中的jar,而java.class.path路径就是我们程序所在的位置,结果返回了我们程序中所有以及所需要使用到的类的所在位置,其中应用程序类加载器主要加载的只有D:\idea_workspace\study\out\production\study;
路径下的类:
/**
* appClassLoader加载以下文件:
* D:\Java\jdk1.8.0_171\jre\lib\charsets.jar;
* D:\Java\jdk1.8.0_171\jre\lib\deploy.jar;
* D:\Java\jdk1.8.0_171\jre\lib\ext\access-bridge-64.jar;
* D:\Java\jdk1.8.0_171\jre\lib\ext\cldrdata.jar;
* D:\Java\jdk1.8.0_171\jre\lib\ext\dnsns.jar;
* D:\Java\jdk1.8.0_171\jre\lib\ext\jaccess.jar;
* D:\Java\jdk1.8.0_171\jre\lib\ext\jfxrt.jar;
* D:\Java\jdk1.8.0_171\jre\lib\ext\localedata.jar;
* D:\Java\jdk1.8.0_171\jre\lib\ext\nashorn.jar;
* D:\Java\jdk1.8.0_171\jre\lib\ext\sunec.jar;
* D:\Java\jdk1.8.0_171\jre\lib\ext\sunjce_provider.jar;
* D:\Java\jdk1.8.0_171\jre\lib\ext\sunmscapi.jar;
* D:\Java\jdk1.8.0_171\jre\lib\ext\sunpkcs11.jar;
* D:\Java\jdk1.8.0_171\jre\lib\ext\zipfs.jar;
* D:\Java\jdk1.8.0_171\jre\lib\javaws.jar;
* D:\Java\jdk1.8.0_171\jre\lib\jce.jar;
* D:\Java\jdk1.8.0_171\jre\lib\jfr.jar;
* D:\Java\jdk1.8.0_171\jre\lib\jfxswt.jar;
* D:\Java\jdk1.8.0_171\jre\lib\jsse.jar;
* D:\Java\jdk1.8.0_171\jre\lib\management-agent.jar;
* D:\Java\jdk1.8.0_171\jre\lib\plugin.jar;
* D:\Java\jdk1.8.0_171\jre\lib\resources.jar;
* D:\Java\jdk1.8.0_171\jre\lib\rt.jar;
* D:\idea_workspace\study\out\production\study;
* D:\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar
*/
那么为什么会这样子呢?这就需要我们了解类加载的双亲委派机制了。继续往下看吧。
Launcher类源码
前面我们了解到,扩展类加载器和应用程序类加载器两者不仅都是JVM启动器示例Launcher类的内部类,而且二者之间还存在着某种关系,而扩展类加载器又和引导类加载器之间存在着某种关系,那么他们究竟是怎么实现的呢?ExtClassLoader对象和AppClassLoader对象又是如何构造的呢?这就需要我们去看看Launcher类的源码详细地了解一下了。
那么,我们来看以下的源码:
//sun.misc包下的Launcher类
package sun.misc;
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
//1.2 getLauncher方法返回的静态变量launcher在加载时已经初始化好(是一个单例),于是我们看看Launcher的构造方法
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
//1.1 getLauncher方法返回的是变量launcher
public static Launcher getLauncher() {
return launcher;
}
//1.3 launcher的构造方法:
public Launcher() {
//1.3.1 定义一个扩展类加载器变量var1
Launcher.ExtClassLoader var1;
try {
//1.3.2 获取扩展类加载器赋给var1,于是我们看内部类ExtClassLoader类的getExtClassLoader()方法
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//2.1 内部类AppClassLoader的getAppClassLoader(final ClassLoader var0)方法传入的参数是上一步的扩展类加载器,返回值赋给ClassLoader loade
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);
}
}
//内部类AppClassLoader继承URLClassLoader类
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
//2.2 内部类AppClassLoader的getAppClassLoader(final ClassLoader var0)方法
//2.2.1 var0就是上面传进来的ExtClassLoader
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
//2.2.2 拿到了java.class.path环境变量的路径赋给var1
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);
//2.2.3 返回的是初始化的应用程序类加载器AppClassLoader(接下看AppClassLoader(URL[] var1, ClassLoader var2)构造方法)
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
//2.3 AppClassLoader的构造方法
AppClassLoader(URL[] var1, ClassLoader var2) {
//2.3.1 调用父类URLClassLoader的构造方法
//URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory)
//var2就是上面的ExtClassLoader
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
}
//内部类ExtClassLoader继承URLClassLoader类
static class ExtClassLoader extends URLClassLoader {
//定义一个扩展类加载器的静态变量instance
private static volatile Launcher.ExtClassLoader instance;
//1.4 Launcher.ExtClassLoader getExtClassLoader()方法
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
if (instance == null) {
Class var0 = Launcher.ExtClassLoader.class;
synchronized(Launcher.ExtClassLoader.class) {
if (instance == null) {
//1.4.1 调用了createExtClassLoader(),将返回值赋给了静态变量instance
instance = createExtClassLoader();
}
}
}
//1.5.2 返回的就是最终初始化的扩展类加载器
return instance;
}
//1.5 Launcher.ExtClassLoader createExtClassLoader()方法
private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
File[] var1 = Launcher.ExtClassLoader.getExtDirs();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
MetaIndex.registerDirectory(var1[var3]);
}
//1.5.1 最终初始化扩展类加载器(Launcher.ExtClassLoader(var1))
return new Launcher.ExtClassLoader(var1);
}
});
} catch (PrivilegedActionException var1) {
throw (IOException)var1.getException();
}
}
//1.6 ExtClassLoader(File[] var1)构造方法
public ExtClassLoader(File[] var1) throws IOException {
//1.6.1 调用父类URLClassLoader的构造方法
//URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory)
//往父类构造方法的ClassLoader parent的值传入的是(ClassLoader)null,也就是传入一个空值null
//由于ExtClassLoader的parent属性按道理是引导类加载器所对应的类,然而引导类加载器的底层是C++实现的,不是一个Java对象,因此为空。
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
}
}
//java.net包下的URLClassLoader类
package java.net;
public class URLClassLoader extends SecureClassLoader implements Closeable {
/* The search path for classes and resources */
private final URLClassPath ucp;
/* The context to be used when loading classes and resources */
private final AccessControlContext acc;
//1.7 ExtClassLoader类调用父类URLClassLoader的构造方法,由1.6知ExtClassLoader的parent属性为空
//2.4 同样的,AppClassLoader也是调用了父类这个构造方法,而ClassLoader parent就是ExtClassLoader
//URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory)
//urls:类名或者磁盘文件地址的路径。通过IO将文件加载到内存
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
//2.4.1 调用父类的构造方法SecureClassLoader(ClassLoader parent),其中parent就是1.3.2的var1,即ExtClassLoader
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);
}
}
//java.security包下的SecureClassLoader类
package java.security;
public class SecureClassLoader extends ClassLoader {
protected SecureClassLoader(ClassLoader parent) {
//2.5 调用父类的构造方法ClassLoader(ClassLoader parent),传入的parent为ExtClassLoader
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
initialized = true;
}
}
//java.lang包下的ClassLoader
package java.lang;
public abstract class ClassLoader {
//2.6 ClassLoader的构造方法,AppClassLoader传入的parent为ExtClassLoader
protected ClassLoader(ClassLoader parent) {
//2.6.1 私有的构造方法private ClassLoader(Void unused, ClassLoader parent)
this(checkCreateClassLoader(), parent);
}
private ClassLoader(Void unused, ClassLoader parent) {
//2.7 AppClassLoader的ClassLoader parent属性就是ExtClassLoader var1
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;
}
}
}
为了看的跟清晰一些,如下为URLClassLoader、SecureClassLoader、ClassLoader三个类之间的继承关系:
由以上的源码我们可以了解到,C++语言会通过Launcher类将扩展类加载器ExtClassLoader对象和应用程序类加载器AppClassLoader对象构造出来,并把二者之间的关系构造好,即AppClassLoader对象的parent属性为ExtClassLoader对象。
双亲委派机制
回到本文一开始,比如现在要使用我们那个Math类,那么它会:
- 先被应用程序类加载器加载,如果应用程序类加载器之前加载过的类中已经Math类,则可以直接返回
- 如果应用程序类加载器之前没有加载过Math类,则会向上委托给它的父加载器——扩展类加载器去加载
- 同理,扩展类加载器也会去检查之前有没有加载过Math类,这当然没有(如果有一样直接返回),所以又会向上委托给它的父加载器引导类加载器进行加载
- 引导类加载器会去检查之前有没有加载过Math类,还是没有(如果有一样直接返回),它会去JVM运行的位于JRE的lib目录下的那些核心类库里面找Math类,这当然还是找不到
- 于是引导类加载器需要去委托扩展类加载器加载,扩展类加载器一样会去那些扩展类中去找Math类,如果找到了就返回,当然还是没有找到
- 于是扩展类加载器委托应用程序类加载器加载Math类,应用程序类加载器会去java.class.path路径下去找,这肯定就找到了,于是调用loadClass(”com.jvm01.Math)方法加载Math方法
由图我们可以看出,双亲委派机制让我们的Java程序在加载类的时候先从应用程序类加载器开始向上委托,我们来看源码:
private ClassLoader loader;
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 2. loader为AppClassLoader
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);
}
}
public ClassLoader getClassLoader() {
// 1. Launcher的getClassLoader()方法,返回loader,loader在初始化LaunCher的时候赋值
return this.loader;
}
在加载类的时候,创建一个JVM启动器实例Launcher类,调用其getClassLoader方法获取到的loader就是AppClassLoader,所以我们一开始加载类的类加载器是AppCalssLoader。
一个应用程序,绝大多数的类都是我们自己编写的,都由我们的应用程序类加载器进行加载;对于我们自己编写的类,虽然在第一次加载的时候很麻烦,需要向上委托扩展类加载器和引导类加载器,然后再最终由应用程序类加载器进行加载,但是以后还需要加载这个类的时候就不用这么麻烦了,直接就可以在应用程序类加载器返回了。这也就是我们一开始加载类的类加载器是AppClassLoader的好处之一。
我们再来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下:
- 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
- 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
- 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。
package java.lang;
public abstract class ClassLoader {
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1. 首先,检查该类是否已经加载
Class<?> c = findLoadedClass(name);
// 2. 如果没有加载过,也就C==null
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 3. 那么就尝试去它的父加载器也就是扩展类加载器上进行加载,也最终调用的是这个loadClass方法,再次执行1检查该类是否已经加载
c = parent.loadClass(name, false);
} else {
// 4. 该类如果在上面的扩展类加载器上没有加载过,则因为其父加载器是应用程序类加载器,为null,所以到这里执行findBootstrapClassOrNull方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果从扩展类加载器和引导类加载器中找不到类,则抛出ClassNotFoundException
}
if (c == null) {
long t1 = System.nanoTime();
// 6. 如果仍然找不到,请调用findClass以便找到该类。首先是扩展类加载器去调用这个findClass方法,因为现在还在3的loadClass里面执行程序,可是ExtClassLoader没有findClass方法,而是使用它的父类URLClassLoader的findClass方法,不过如果不是扩展目录的类是加载不成功的,所以最终返回null,来到AppClassLoader调用的loadClass方法
// 7. 由于父加载器返回null,所以AppClassLoad执行父类URLClassLoader的findClass方法,经历了加载、验证、准备、解析、初始化之后最终完成类的
c = findClass(name);
// 这是定义类加载器;记录统计数据
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
/**
* 5. 返回一个由引导类加载器加载的类;如果没有找到,则返回null。
*/
private Class<?> findBootstrapClassOrNull(String name)
{
// 5.1 如果名称不存在,则返回null
if (!checkName(name)) return null;
// 5.2 否则调用findBootstrapClass返回一个由引导类加载器加载的类
return findBootstrapClass(name);
}
// 5.1 如果名称为null或可能是一个有效的二进制名称,则为true
private boolean checkName(String name) {
if ((name == null) || (name.length() == 0))
return true;
if ((name.indexOf('/') != -1)
|| (!VM.allowArraySyntax() && (name.charAt(0) == '[')))
return false;
return true;
}
// 5.2 如果没有找到就返回null
private native Class<?> findBootstrapClass(String name);
/**
* 查找具有指定二进制名称的类。
* 该方法应由遵循加载类的委托模型的类加载器实现覆盖,并将在检查所请求类的父类加载器后由loadClass方法调用。
* 默认实现抛出ClassNotFoundException。
* 类的二进制名称返回:如果在以下情况下无法找到该类,则结果类对象抛出:ClassNotFoundException
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
public class URLClassLoader extends SecureClassLoader implements Closeable {
/**
* 从URL搜索路径查找并加载指定名称的类。任何指向JAR文件的url都会根据需要加载并打开,直到找到该类。
*
* @param 类的名称
* @return 结果类
* @exception 如果类找不到,或如果加载器关闭则为ClassNotFoundException
* @exception 如果{@code name}是{@code null}则为NullPointerException
*/
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
// path就是类路径
String path = name.replace('.', '/').concat(".class");
// 通过类路径找到文件res
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
// defineClass方法把类文件res进行加载、验证、准备、解析、初始化的操作
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
/*
* 使用从指定资源获得的类字节定义类。在使用生成的类之前,必须先解析该类
*/
private Class<?> defineClass(String name, Resource res) throws IOException {
long t0 = System.nanoTime();
int i = name.lastIndexOf('.');
URL url = res.getCodeSourceURL();
if (i != -1) {
String pkgname = name.substring(0, i);
// Check if package already loaded.
Manifest man = res.getManifest();
definePackageInternal(pkgname, man, url);
}
// 现在读取类字节并定义类
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
// 使用(直接)ByteBuffer
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, bb, cs);
} else {
byte[] b = res.getBytes();
// 读取字节后必须读取证书。
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, b, 0, b.length, cs);
}
}
}
为什么设计双亲委派
- 沙箱安全机制:自己写的核心类、扩展类如java.lang.String.class不会被加载,这样便可以防止核心API库被随意篡改
- 避免类的重复加载:当父加载器已经加载该类时,就没有必要让子加载器再加载一次,保证被加载类的唯一性。
对于沙箱安全机制,几个例子:
例子一核心库类:
package java.lang;
/**
* @author LinKai
* @time 2021/03/05-18:15:00
*/
public class String {
public static void main(String[] args) {
System.out.println("java.lang.String");
}
}
运行结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
例子二扩展类:
package com.sun.crypto.provider;
/**
* @author LinKai
* @time 2021/03/05-18:19:00
*/
public class DESKeyFactory {
public static void main(String[] args) {
System.out.println("com.sun.crypto.provider.DESKeyFactory");
}
}
运行结果:
错误: 在类 com.sun.crypto.provider.DESKeyFactory 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
通过上面的例子我们可以知道,在双亲委派机制下,我们无法通过编写一个与核心库类或扩展类同名的类来篡改JDK为我们提供的Java类,也因此保证了Java的安全性。
全盘负责委托机制
全盘负责是指当一个ClassLoader装载一个类时,除非显示地指定使用另外一个ClassLoader,该类所依赖所引用的类也由这个ClassLoader载入。
自定义类加载器
刚才我们通过源码了解到,在类的加载过程最核心就是就是这么两个方法:一个是ClassLoader类里的loadClass方法,它实现了双亲委派机制;另一个是ClassLoader类里的findClass方法,虽然是一个空方法,但是其子类URLClassLoader重写该方法实现了通过类名找到类的所在路径、获取类文件,然后对类进行加载、验证、准备、解析、初始化的一系列操作。
所以我们的自定义类加载器只需要继承java.lang.ClassLoader类,重写findClass方法:
package com.example.jvm.classload;
import java.io.FileInputStream;
import java.lang.reflect.Method;
/**
* 自定义类加载器:
* 1. 首先需要继承ClassLoader类
* 2. 重写findClass方法
*
* @author LinKai
* @time 2021/03/05-19:14:00
*/
public class MyClassLoader extends ClassLoader {
private final String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
public static void main(String[] args) throws Exception {
// 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/idea_workspace/test");
// 在D盘创建文件目录:test/com/example/jvm/classload,将Test.class文件放到该目录中
// 然后加载该类:
Class clazz = classLoader.loadClass("com.example.jvm.classload.Test");
// 反编译
Object o = clazz.newInstance();
Method m = clazz.getDeclaredMethod("test", null);
// 执行Test类的test方法
m.invoke(o, null);
// 打印Test类的加载器
System.out.println(
"Test类的加载器:" +
clazz.getClassLoader().getClass().getName()
);
}
/**
* 通过类名得到该类文件的字节数组
*
* @param name
* @return
* @throws Exception
*/
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
/**
* 1. 根据要加载的类的类名,到磁盘找到该类的文件转化为字节数组data
* 2. 调用父类的defineClass方法完成类的加载、验证、准备、解析和初始化的操作
*
* @param name 要加载的类的类名
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
// defineClass将一个字节数组转为Class对象,这个字节数组是Class文件读取后最终的字节数组
/*
参数1 name:预期的类的二进制名称;如果未知,则为null
参数2 b:组成类数据的字节。从off到off + len-1的位置中的字节应具有Java™虚拟机规范所定义的有效类文件的格式。
参数3 off:类数据b中的起始偏移量
参数4 len:类数据的长度
*/
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
值得一提的是,我们自己实现的自定义类加载器,它默认的父加载器(也就是它的parent属性)是应用程序类加载器AppClassLoader。
顺便贴上我们的Test类:
package com.example.jvm.classload;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author LinKai
* @time 2021/03/11-14:21:00
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Test {
private int id;
private String massage;
public void test() {
System.out.println("=====自定义类加载器加载Test类======");
}
}
我们运行代码看看结果:
=====自定义类加载器加载Test类======
Test类的加载器:sun.misc.Launcher$AppClassLoader
那么问题来了,我们不是已经自定义了类加载器了吗,为什么加载Test类的类加载器还是AppClassLoader呢?
因为双亲委派机制。我们自定义的类加载器MyClassLoader它的父加载器是AppClassLoader,因为是第一次加载这个类,一开始类加载器向上委托它们的父加载器去加载Test类,直到引导类加载器、扩展类加载器发现它所加载的类路径下没有这个Test类,所以向下委托给应用程序类加载器,应用程序类加载器去尝试加载的时候刚好就找到了,所以由应用程序类加载器来加载我们的Test类。
我们只需要把项目中的Test类删除,应用程序类加载器就找不到Test类,只能交给我们的自定义类加载器MyClassLoader了。
我们再看看把项目中的Test类删除之后的结果:
=====自定义类加载器加载Test类======
Test类的加载器:com.example.jvm.classload.MyClassLoader
虽然说知道了我们自定义的类加载器它的父加载器是AppClassLoader,但是总得知道原因吧。让我们再看看源码就知道了:
// 因为初始化MyClassLoader之前需要先初始化它的父类ClassLoader,所以我们按照顺序先从ClassLoader的构造方法下手:
package java.lang;
public abstract class ClassLoader {
/**
* 使用方法getSystemClassLoader()返回的ClassLoader作为父类加载器,创建一个新的类加载器。
* 如果有安全管理器,则调用其checkCreateClassLoader方法。这可能会导致安全异常。
* 抛出:SecurityException
* 如果安全管理器存在并且其checkCreateClassLoader方法不允许创建新的类加载器。
*/
protected ClassLoader() {
// 1. 初始化自定义类加载器首先会执行这个ClassLoader的构造方法,它调用了private ClassLoader(Void unused, ClassLoader parent)方法
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
// 2. 我们发现它的parent属性就是上一步构造方法中getSystemClassLoader()的返回值
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
assertionLock = this;
}
}
// 3. 于是我们看看这个getSystemClassLoader()方法,它返回scl也就是最终的parent
public static ClassLoader getSystemClassLoader() {
// 4. 它调用了initSystemClassLoader()方法为scl赋值
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;
// 5. 在这一步,它调用了Launcher类的getClassLoader()方法得到scl,在前面我们就知道了Launcher类的getClassLoader()方法它的返回值就是AppClassLoader,所以,自定义类加载器默认的parent就是AppClassLoader
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;
}
}
}
打破双亲委派机制
现在我们开始尝试实现一个自定义类加载器来打破双亲委派机制,让我们的自定义类加载器在加载类的时候不是先去向上委托它的父加载器而是直接到指定的目录下去加载类。
在前文我们知道类加载的双亲委派机制就在ClassLoader类的loadClass方法中实现,那么我们想要打破双亲委派机制就需要重写loadClass方法,把父类的loadClass方法中的双亲委派实现删除掉:
package com.example.jvm.classload;
import java.io.FileInputStream;
import java.lang.reflect.Method;
/**
* 实现一个自定义类加载器打破双亲委派
* 1. 继承ClassLoader
* 2. 重写findClass方法
* 3. 打破双亲委派的重点在于重写loadClass方法,
* 因为双亲委派就是在该方法中实现的,所以我们只需要把原来的loadClass中的双亲委派代码删除即可
*
* @author LinKai
* @time 2021/03/12-18:46:00
*/
public class MyClassLoaderWithoutParents extends ClassLoader {
private final String classPath;
public MyClassLoaderWithoutParents(String classPath) {
this.classPath = classPath;
}
public static void main(String[] args) throws Exception {
// 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoaderWithoutParents classLoader = new MyClassLoaderWithoutParents("D:/idea_workspace/test");
// 在D盘创建文件目录:test/com/example/jvm/classload,将Test.class文件放到该目录中
// 然后加载该类:
Class clazz = classLoader.loadClass("com.example.jvm.classload.Test");
// 反编译(是反射,打错字了,下面的截图也是)
Object o = clazz.newInstance();
Method m = clazz.getDeclaredMethod("test", null);
// 执行Test类的test方法
m.invoke(o, null);
// 打印Test类的加载器
System.out.println(
"Test类的加载器:" +
clazz.getClassLoader().getClass().getName()
);
}
@Override
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();
// 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;
}
}
/**
* 通过类名得到该类文件的字节数组
*
* @param name
* @return
* @throws Exception
*/
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
/**
* 1. 根据要加载的类的类名,到磁盘找到该类的文件转化为字节数组data
* 2. 调用父类的defineClass方法完成类的加载、验证、准备、解析和初始化的操作
*
* @param name 要加载的类的类名
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
// defineClass将一个字节数组转为Class对象,这个字节数组是Class文件读取后最终的字节数组
/*
参数1 name:预期的类的二进制名称;如果未知,则为null
参数2 b:组成类数据的字节。从off到off + len-1的位置中的字节应具有Java™虚拟机规范所定义的有效类文件的格式。
参数3 off:类数据b中的起始偏移量
参数4 len:类数据的长度
*/
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
运行结果:
java.io.FileNotFoundException: D:\idea_workspace\test\java\lang\Object.class (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
因为我们自定义的类加载器虽然打破双亲委派,直接就到指定的目录上加载类,但是也正因为这样,加载类的时候需要先加载它的父类Object,而在我们自定义的目录下却没有Object类,所以就会报这样子的错误。
那么如何解决呢?两个方案:
- 我们把JDK中的Object类复制到指定的目录下;
- 自定义类加载器所要加载的类自己加载,其他类让JDK提供的类加载器在双亲委派机制下进行加载。
我们先尝试方案1,把Object类复制粘贴放到D:/idea_workspace/test/java/lang目录下,结果如下:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
我们也可以把之前自己写的java.lang.String类放到上面的目录下,用我们打破了双亲委派的类加载器去加载它,也是报同样的错误。这是因为前面所说的沙箱安全机制:JDK为我们提供的核心类和扩展类分别只能由引导类加载器和扩展类加载器去加载。
那么我们只能尝试方案2,如图,成功了:
// 把原来的c = findClass(name);换成下面的代码:
if (!name.startsWith("com.example.jvm.classload")) {
// 如果不是自定义类加载器所要加载的类,就交给父加载器去加载(也就按照双亲委派去加载该类了)
c = this.getParent().loadClass(name);
} else {
// 否则的话,就用我们自己实现的类加载器去加载就行
c = findClass(name);
}
这样子,我们要加载的类直接就可以打破双亲委派让自己的加载器直接加载,而其他所要依赖到的相关的类依旧是要按照双亲委派去加载。
补充:Hotspot源码JVM启动执行main方法流程
扩:Tomcat打破双亲委派
Tomcat在类加载的时候为什么需要打破双亲委派呢?
Tomcat是一个web应用容器:
- 一个web应用容器可能需要部署两个及以上的应用程序(要是只能部署一个应用程序就太没用了),不同的应用程序可能也会依赖到同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每一个应用程序所依赖的不同的类库都是独立的,保证相互隔离。
- 部署在同一个web容器中相同的类库的相同版本可以共享。不然如果服务器多大量的应用程序就会有大量的相同类库加载进虚拟机,这就太浪费内存了。
- web容器也有自己依赖的类库,出于安全考虑,web容器的类库不能与应用程序的类库混淆,应该将二者隔离开来。
- web容器需要支持jsp的修改,我们知道,jsp文件最终也是要编译成.class文件才可以在虚拟机上运行的,但程序运行后修改jsp已经是司空见惯的事情,web容器需要支持jsp修改后不用重启。
那么,Tomcat就不能使用默认的双亲委派加载机制:
- 如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加载器是不管版本只关注全限定类名的,并且只有一份;
- 要实现jsp的热加载也是个问题,jsp最终编译为class文件,修改jsp之后因为类名还是不变的,此时类加载器会直接取方法区中已经存在的类,所以不能实现jsp的热加载。要想实现jsp热加载,我们可以直接卸载这个jsp文件的类加载器,因此我们需要打破双亲委派,让每一个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器,然后重新创建类加载器重新加载jsp文件。
Tomcat自定义类加载器
Tomcat几个主要的类加载器:
- CommonClassLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问
- CatalinaClassLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见
- SharedClassLoader:各个Webapp共享的类加载器,加载路径中的class对于Webapp可见,但是对于Tomcat容器不可见
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class对于当前的Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现能加载各自的spring版本
从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。
WebappClassLoader可以使用SharedClassLoader加载到的类,但各个WebappClassLoader实例之间相互隔离(一个应用程序对应一个WebappClassLoader)。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那个Servlet文件,它出现的目的就是为了:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的JSP类加载器来实现JSP文件的热加载功能。
Tomcat为了实现隔离性,违背了Java推荐的双亲委派模型,每个WebappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。
模拟WebappClassLoader
我们尝试实现WebappClassLoader加载自己的war包应用内不用版本的类的相互共存与隔离:
package com.example.jvm.classload;
import java.io.FileInputStream;
import java.lang.reflect.Method;
/**
* 实现Tomcat的WebappClassLoader
*
* @author LinKai
* @time 2021/03/18-17:31:00
*/
public class MyWebappClassLoader extends ClassLoader {
private final String classPath;
public MyWebappClassLoader(String classPath) {
this.classPath = classPath;
}
public static void main(String[] args) throws Exception {
MyWebappClassLoader classLoader = new MyWebappClassLoader("D:/idea_workspace/test");
Class clazz = classLoader.loadClass("com.example.jvm.classload.Test");
Object o = clazz.newInstance();
Method m = clazz.getDeclaredMethod("test", null);
m.invoke(o, null);
System.out.println(clazz.getClassLoader());
System.out.println("======================");
MyWebappClassLoader classLoader1 = new MyWebappClassLoader("D:/idea_workspace/test0");
Class clazz1 = classLoader1.loadClass("com.example.jvm.classload.Test");
Object o1 = clazz1.newInstance();
Method m1 = clazz1.getDeclaredMethod("test", null);
m1.invoke(o1, null);
System.out.println(clazz1.getClassLoader());
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int lens = fis.available();
byte[] data = new byte[lens];
fis.read(data);
fis.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t1 = System.nanoTime();
if (!name.startsWith("com.example.jvm.classload")) {
c = this.getParent().loadClass(name);
} else {
c = findClass(name);
}
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
test0目录下的test,作为之前test的另一个版本:
package com.example.jvm.classload;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author LinKai
* @time 2021/03/11-14:21:00
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Test {
private int id;
private String massage;
public void test() {
System.out.println("=====自定义类加载器加载test0目录下的Test类======");
}
}
运行结果:
注意:
在同一个JVM内是可以有两个相同类名以及包名的类对象相互共存的,因为它们的类加载器可以不一样,所有要看两个类对象是否是同一个,除了看两个类对象它们的类名和包名是否一致外,还需要看它们是否是同一个类加载器加载的。
JasperLoader热加载原理
后台启动一个线程监听jsp文件的变化,如果变化就找到该jsp对应的servlet类的加载器引用(gcroot),重新生成新的JasperLoader加载器赋值引用,然后加载新的jsp对应的servlet类,之前的加载器因为没用gcroot引用了,下次gc会被销毁。
补充
刚才在和朋友讨论的时候发现这样一个问题:
// 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
MyClassLoaderWithoutParents classLoader = new MyClassLoaderWithoutParents("D:/idea_workspace/test");
// 在D盘创建文件目录:test/com/example/jvm/classload,将Test.class文件放到该目录中
// 然后加载该类:
Class clazz = classLoader.loadClass("com.example.jvm.classload.Test");
// 反射
Object o = clazz.newInstance();
Method m = clazz.getDeclaredMethod("test", null);
// 执行Test类的test方法
m.invoke(o, null);
// 打印Test类的加载器
System.out.println("Test类的加载器:" +clazz.getClassLoader().getClass().getName());
我们知道,类是在defineClass方法中完成类的验证、准备、解析、初始化的,然后loadClass方法返回了这个加载的类的Class对象。
问:返回的这个对象再进行反射,这个类会不会被重复加载呢?为什么?
调试的结果是不会被重复加载;
在理论上即使重复的new同一个类的对象也只会加载一次这个类,只会多次执行实例化的步骤。
loadClass方法返回的Class对象是为了方便获取被加载的类的实例对象。
喜欢这期的小伙伴还请点赞、收藏、三连一下,你们的支持是我们创作的动力;对这期内容有不同看法的小伙伴也可以在评论区留下不同的看法或宝贵的意见,谢谢你们♥