关于从JDK源码剖析JVM类加载机制
一、Java指令运行程序流程
- 首先通过类加载器将字节码文件加载到 JVM 虚拟机中,以自定义 Math 类为例:java com.autumn.Math.class
public class Math {
public static int num = 123;
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();
}
}
- 以 Windows 系统为例,java 指令即为 C++ 语言编写的 java.exe,它会调用底层 C++ 的库函数 jvm.dll 文件来创建 Java 的虚拟机
- 虚拟机会创建一个引导类加载器实例
- C++ 接着会调用 Java 代码创建 JVM 启动程序,其中的 sun.misc.Launcher 会由引导类加载器负责加载创建很多 Java 实现的类加载器:sun.misc.Launcher.getLauncher()
- 再获取运行类自己的加载器 ClassLoader,Math 类的加载器获取结果为 AppClassLoader:launcher.getClassLoader()
- 调用 loadClass 方法来加载磁盘上的字节码文件 Math 类:classLoader.loadClass(“com.autumn.Math.class”)
- 加载完成后 C++ 调用 JVM 执行 Math 类的 main 方法:Math.main()
- 程序运行结束,JVM 销毁
二、loadClass类加载过程
- 加载:加载磁盘里的字节码文件,即 target 目录下的文件,使用到对应的类时才会加载,例如调用 main 方法,创建对象等,在内存中生成这个类的 java.lang.Class 对象,作为方法区此类各种数据的访问入口
字节码文件默认开头:cafe babe
- 验证:验证字节码文件是否格式正确
- 准备:为静态变量做初始值赋值
例如:int 初始值为 0,boolean 初始值为 false,对象为 null
- 解析:将符号引用替换为直接引用,main 方法和一些静态方法替换为指向数据所存内存的指针或句柄,即为静态链接的过程,在类加载期间完成
符号:指代码里的修饰符、返回值、方法名、参数、类名等
静态链接:当这些符号加载到内存时所对应的地址
动态链接:加载时候不一定会解析成内存地址,直到运行到的时候才解析
将字节码文件分解成可读性更好的格式的指令:javap -v Math.class
生成的文件里面#数字所对应的字段即为符号
Classfile /D:/SelfProject/demo/target/classes/com/autumn/jvm/Math.class
Last modified 2022-10-1; size 753 bytes
MD5 checksum 7fa2b091a0b6bbf85f53b67854dca927
Compiled from "Math.java"
public class com.autumn.jvm.Math
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#34 // java/lang/Object."<init>":()V
#2 = Class #35 // com/autumn/jvm/Math
#3 = Methodref #2.#34 // com/autumn/jvm/Math."<init>":()V
#4 = Methodref #2.#36 // com/autumn/jvm/Math.compute:()I
#5 = Fieldref #2.#37 // com/autumn/jvm/Math.num:I
#6 = Class #38 // com/autumn/jvm/User
#7 = Methodref #6.#34 // com/autumn/jvm/User."<init>":()V
#8 = Fieldref #2.#39 // com/autumn/jvm/Math.user:Lcom/autumn/jvm/User;
#9 = Class #40 // java/lang/Object
#10 = Utf8 num
#11 = Utf8 I
#12 = Utf8 user
#13 = Utf8 Lcom/autumn/jvm/User;
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 Lcom/autumn/jvm/Math;
#21 = Utf8 compute
#22 = Utf8 ()I
#23 = Utf8 a
#24 = Utf8 b
#25 = Utf8 c
#26 = Utf8 main
#27 = Utf8 ([Ljava/lang/String;)V
#28 = Utf8 args
#29 = Utf8 [Ljava/lang/String;
#30 = Utf8 math
#31 = Utf8 <clinit>
#32 = Utf8 SourceFile
#33 = Utf8 Math.java
#34 = NameAndType #14:#15 // "<init>":()V
#35 = Utf8 com/autumn/jvm/Math
#36 = NameAndType #21:#22 // compute:()I
#37 = NameAndType #10:#11 // num:I
#38 = Utf8 com/autumn/jvm/User
#39 = NameAndType #12:#13 // user:Lcom/autumn/jvm/User;
#40 = Utf8 java/lang/Object
{
public static int num;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public static com.autumn.jvm.User user;
descriptor: Lcom/autumn/jvm/User;
flags: ACC_PUBLIC, ACC_STATIC
public com.autumn.jvm.Math();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/autumn/jvm/Math;
public int compute();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 14: 0
line 15: 2
line 16: 4
line 17: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/autumn/jvm/Math;
2 11 1 a I
4 9 2 b I
11 2 3 c I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/autumn/jvm/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: return
LineNumberTable:
line 21: 0
line 22: 8
line 23: 13
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 args [Ljava/lang/String;
8 6 1 math Lcom/autumn/jvm/Math;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: bipush 123
2: putstatic #5 // Field num:I
5: new #6 // class com/autumn/jvm/User
8: dup
9: invokespecial #7 // Method com/autumn/jvm/User."<init>":()V
12: putstatic #8 // Field user:Lcom/autumn/jvm/User;
15: return
LineNumberTable:
line 10: 0
line 11: 5
}
SourceFile: "Math.java"
- 初始化:将静态变量初始化为指定的值,以及执行静态代码块
- 使用
- 卸载
类被加载到方法区后主要包含:运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
类加载器的引用:这个类到类加载器实例的引用
对应 class 实例的引用:类加载器在加载类信息到方法区后,会创建一个对应的 Class 类型的对象实例(java.lang.Class对象),放到堆中,作为开发人员访问方法区中类定义的入口和切入点
类加载属于懒加载,主类在运行过程中若使用到其它类,才逐步加载这些类。
因此 jar 包或 war 包里的类并非一次性加载,而是使用时才会加载。
加载顺序:主类的静态代码块,main 方法里按顺序执行到便加载,若 new 对象则加载,若对象声明为 null 不加载
三、类加载器和双亲委派机制
上述类加载过程均由类加载器实现
- 引导类加载器 BootstrapClassLoader:加载支撑 JVM 运行的位于 jre 的 lib 目录下的核心类库,例如 rt.jar、charset.jar 等
String.class.getClassLoader()
String 属于核心类库,引导类均为 C++ 生成的对象,所以无法用 Java 获取,结果为 null
- 扩展类加载器 ExtClassLoader:加载支撑 JVM 运行的位于 jre 的 lib 目录下的 ext 扩展目录中的 jar 包
com.sun.crypto.provider.DESKeyFactory.class.getClassLoader()
结果为 sun.misc.Launcher$ExtClassLoader@xxxxxxxx
- 应用程序类加载器 AppClassLoader:加载 ClassPath 路径下的类包,即自己写的类
TestJDKClassLoader.class.getClassLoader()
此类为自己写的类,结果为 sun.misc.Launcher$AppClassLoader@xxxxxxxx
- 自定义加载器:加载用户自定义路径下的类包
Launcher 类为 rt.jar 包里最核心的类,继承 URLClassLoader。
Launcher 类初始化过程,调用 getLacuncher():
返回 launcher,launcher 在静态变量里 new Launcher(),即为单例对象;
Launcher 类构造方法:
①定义一个扩展类加载器:Launcher.ExtClassLoader
②调用 getExtClassLoader 方法来获取类加载器
扩展类加载器的初始化 getExtClassLoader:
经过校验,返回 new Launcher.ExtClassLoader(),即初始化一个扩展类加载器;
初始化时会调用父类加载器 URLClassLoader,根据传入的磁盘路径,进行文件读写,加载到内存中。
③调用应用程序类加载器的 getAppClassLoader 方法,将扩展类加载器作为参数
先获取环境变量:System.getProperty(“java.class.path”),经过一些操作,最终返回 new Launcher.AppClassLoader(),即初始化一个应用程序类加载器;
初始化的 AppClassLoader 会调用父构造方法,即 URLClassLoader,作用同上。
ClassLoader.getSyetemClassLoader()即为appClassLoader:系统类生成的类加载器,为应用程序类加载器
sun.misc.Launcher$AppClassLoader@xxxxxxxx
加载了环境变量下类路径里的包:System.getProperty(“java.class.path”)
虽然 appClassLoader 会扫描类路径里所有的包,但只会去加载 target 目录下的包
appClassLoader.getParent()即为extClassLoader:应用程序类加载器的上级加载器为扩展类加载器
sun.misc.Launcher$ExtClassLoader@xxxxxxxx
加载了环境变量下的 ext 目录:System.getProperty(“java.ext.dirs”)
extClassLoader.getParent()即为bootstrapLoader:扩展类加载器的上级加载器为引导类加载器
结果为 null,实际上加载了引导类加载器的内路径包:Launcher.getBootstrapClassPath().getURLs(),结果为 URL 数组
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());
System.out.println(TestJDKClassLoader.class.getClassLoader());
System.out.println("----------------------------------------");
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassLoader = appClassLoader.getParent();
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("the bootstrapClassLoader >>> " + bootstrapClassLoader);
System.out.println("the extClassLoader >>> " + extClassLoader);
System.out.println("the appClassLoader >>> " + appClassLoader);
System.out.println("----------------------------------------");
System.out.println("bootstrapClassLoader 加载以下文件:");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL url : urLs) {
System.out.println(url);
}
System.out.println("----------------------------------------");
System.out.println("extClassLoader 加载了以下文件:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("----------------------------------------");
System.out.println("appClassLoader 加载了以下文件:");
System.out.println(System.getProperty("java.class.path"));
//运行结果如下
/*
null
sun.misc.Launcher$ExtClassLoader@33c7353a
sun.misc.Launcher$AppClassLoader@18b4aac2
----------------------------------------
the bootstrapClassLoader >>> null
the extClassLoader >>> sun.misc.Launcher$ExtClassLoader@33c7353a
the appClassLoader >>> sun.misc.Launcher$AppClassLoader@18b4aac2
----------------------------------------
bootstrapClassLoader 加载以下文件:
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_301/jre/classes
----------------------------------------
extClassLoader 加载了以下文件:
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
----------------------------------------
appClassLoader 加载了以下文件:
C:\Program Files\Java\jdk1.8.0_301\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\deploy.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\cldrdata.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\jaccess.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\localedata.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunec.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunmscapi.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\ext\zipfs.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\jfxswt.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\management- agent.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_301\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_301\jre\lib\rt.jar;D:\SelfProject\demo\target\classes;
D:\.m2\repository\org\projectlombok\lombok\1.18.24\lombok-1.18.24.jar;
D:\.m2\repository\com\google\guava\guava\21.0\guava-21.0.jar;C:\Program Files\JetBrains\IntelliJ IDEA 2021.3\lib\idea_rt.jar
*/
类加载器初始化过程
先创建 JVM 启动器实例 sun.misc.Launcher:Launcher 初始化为单例模式,保证一个 JVM 仅有一个 Launcher 实例;
Launcher 构造方法内,分别创建了两个类加载器:sun.misc.Launcher.ExtClassLoader 扩展类加载器、sun.misc.Launcher.AppClassLoader 应用程序类加载器;
JVM 默认使用 Launcher.getClassLoader() 返回的 AppClassLoader 的实例加载自己写的应用程序。
public Launcher() {
Launcher.ExtClassLoader var1;
try{
//创建扩展类加载器,并将上级加载器置为 null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try{
//创建应用程序类加载器,并将上级加载器置为 ExtClassLoader
//Launcher的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");
//以下省略 ……
}
双亲委派机制
当运行类进行加载时,会先通过应用程序类加载器,若应用程序类加载器已加载的包中已包含此类,则会直接加载返回,若不存在则向上委托给扩展类加载器;
扩展类加载器已加载的包中若存在此类则加载返回,若不存在则再向上委托给引导类加载器;
引导类加载器已加载的包中若存在此类则加载返回,若不存在则引导类加载器会尝试去内路径包内寻找,若仍旧寻找不到,则向下委托给扩展类加载器;
扩展类加载器则会尝试去环境变量下 ext 目录下寻找,若仍旧寻找不到,则再向下委托给应用程序类加载器,应用程序类加载器则会尝试去环境变量下 target 目录下寻找,即 loadClass 方法,找到后即加载返回。
上下级类加载器非继承关系,根据 parent 属性来进行委托,继承自 URLClassLoader,调用的是 URLClassLoader 的 loadClass 方法,URLClassLoader 再调用父类的 loadClass 方法,直到调用到根父类 ClassLoader 的 loadClass 方法。
绝大多数类都是属于程序类加载器进行加载的,虽然第一次加载步骤繁琐,但之后再次加载时,便能在第一轮程序类加载器中直接加载返回,从而提高加载性能
LoadClass 逻辑:
- 先检查指定名称的类是否已被当前类加载器加载过,若加载过则直接返回;
- 若未加载过,则判断上级类加载器是否存在,若存在则委托上级类加载器去加载,即调用 parent.loadClass(name, false);
- 若不存在则说明已经是最上级类加载器,即引导类加载器 bootstrapClassLoader,进行加载;
- 若上级类加载器与引导类加载器都找不到该类,则调用当前类的 findClass 方法来完成加载。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {// 例如name为"com.autumn.jvm.Math"
// 当前类加载器会先检查是否已加载过该类,最终会调用到 C++ 写的本地方法来执行
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 若当前加载器的上级不为空,则委托上级加载器加载该类
c = parent.loadClass(name, false);
} else {
//若为空则说明到顶层类加载器了,即引导类加载器,委托其加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 会调用父类 URLClassLoader的findClass 方法,在加载器的类路径里查找并加载该类
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;
}
}
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
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;
}
设计双亲委派机制用意:
1. 沙箱安全机制:
自己写的类若与自带的类重名,例如 java.lang.String.class,此时该类不会被加载,以此来防止核心 API 库被任意篡改。
2. 避免类的重复加载:
当下级类加载器已加载指定类时,就不会让上级类加载器再加载一次,以此保证被加载的类的唯一性。
public class String {
public static void main(String[] args) {
System.out.println("====== This is my String ======");
}
}
运行时报如下错误:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 否则 JavaFX 应用程序类必须扩展javafx.application.Application
全盘负责委托机制:
当一个类加载器加载一个指定类时,除非该类显式地使用另一个类加载器,否则该类所有的依赖和引用都仅用此类加载器加载。
四、自定义类加载器:
- 需继承 ClassLoader;
- 两大核心方法:
①loadClass(String, boolean),实现了双亲委派机制;
②findClass(String),默认实现为空方法,需进行重写
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
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;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
// defineClass 将一个字节数组转为 Class 对象,此字节数组是 class 文件读取后最终的字节数组
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception {
// 初始化自定义类加载器,先初始化上级加载器 ClassLoader,
// 期间将自定义类加载器的上级类加载器设置为应用程序类加载器 AppClassLoader
MyClassLoader myClassLoader = new MyClassLoader("D:/demo");
// D盘创建多级目录:demo/com/autumn/jvm,将 User1 类放入该目录
Class clazz = myClassLoader.loadClass("com.autumn.jvm.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
}
package com.autumn.jvm;
public class User {
private int id;
private String name;
static {
System.out.println("====== load User ======");
}
public User() {
}
public User(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void sout(){
System.out.println("自定义加载器加载类调用方法");
}
}
当项目中有 User1.class 时,说明应用程序类加载器 AppClassLoader 已经加载过此类了,所以在加载 D 盘的 User1.class 时,可在 AppClassLoader 找到,并直接返回,因此运行结果如下:
====== load User ======
------ 自定义加载器加载类调用方法 ------
sun.misc.Launcher$AppClassLoader
当项目中没有 User1.class 时,说明 AppClassLoader 没有加载过此类,上级类加载器也更不会有,因此最终又会向下委托,直到自定义加载器自行加载,因此运行结果如下:
====== load User ======
------ 自定义加载器加载类调用方法 ------
com.autumn.jvm.MyClassLoaderTest$MyClassLoader
五、打破双亲委派机制
用自定义类加载器加载指定的类,当项目存在该类时也不委托给应用程序类加载器,因此要重写 loadClass 方法:
@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();
// 去掉此处的向上委托,即可打破双亲委派
/*try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}*/
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 加载时需加载核心类 Object,而核心类只能由引导类加载器来加载
// 当要加载的类不是自己写的类时,向上委托,否则直接加载,即完成了打破双亲委派
if (!name.startsWith("com.autumn.jvm")) {
c = this.getParent().loadClass(name);
} else {
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;
}
}
执行结果:
====== load User ======
------ 自定义加载器加载类调用方法 ------
com.autumn.jvm.MyClassLoaderTest$MyClassLoader
六、Tomcat打破双亲委派机制
Tomcat 是个 web 容器,需要解决以下几个问题:
- 一个 web 容器可能需部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,需要保证每个应用程序的类库互相独立、互相隔离。
- 部署在同一个 web 容器中相同的类库相同的版本可以共享,否则若服务器有多个应用程序还要多个相同类库加载进虚拟机。
- web 容器也有自己依赖的类库,不能与应用程序的类库混淆。将容器的类库和应用程序的类库隔离开来保证安全。
- web 容器需支持 jsp 的修改,因为 jsp 文件最终也是编译成 class 文件加载进虚拟机中运行,但程序运行后修改 jsp 是司空见惯的事,因此 web 容器需支持 jsp 修改后不用重启。
Tomcat 若使用默认的双亲委派机制无法解决上述问题:
- 双亲委派无法加载两个相同类库的不同版本,默认的类加载器不论什么版本,只保存一份全限定类名。
- 默认的类加载器可以实现同一个 web 容器中共享相同的类库相同的版本,其职责就是确保唯一性。
- 原因同 1,无法解决第 3 个问题。
- 若要 web 容器支持 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 版本。
Tomcat 几个类加载器的委派关系:
- CommonClassLoader 能加载的类都可以被 CatalinaClassLoader和SharedClassLoader 使用,从而实现公有类库的共用,而 CatalinaClassLoader 和 SharedClassLoader 自己加载的类则互相隔离。
- WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 的实例互相隔离。
- JasperLoader 的加载范围仅限于此 jsp 文件所编译出来的 class 文件,当 Web 容器检测到 jsp 文件被修改时,会立即丢弃目前的 JasperLoader 实例,重新创建一个新的进行加载,以此来实现 jsp 文件的热加载功能。
Tomcat 每个 WebAppClassLoader 加载自己目录下的 class 文件,不会传递给上级类加载器,打破双亲委派机制。
模拟实现 Tomcat 的 WebAppClassLoader 加载自己 war 包应用内不同版本类实现互相隔离共存的效果:
复制一份 User1,将 sout 方法里的打印内容做区分,放在 demo1 目录下,在前面已经打破双亲委派机制的自定义类加载器中创建新的实例加载 demo1 目录下的同一个类名
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("D:/demo");
Class clazz = myClassLoader.loadClass("com.autumn.jvm.User1");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
MyClassLoader myClassLoader1 = new MyClassLoader("D:/demo1");
Class clazz1 = myClassLoader1.loadClass("com.autumn.jvm.User1");
Object obj1 = clazz1.newInstance();
Method method1 = clazz1.getDeclaredMethod("sout", null);
method1.invoke(obj1, null);
System.out.println(clazz1.getClassLoader().getClass().getName());
}
执行结果:
====== load User ======
------ 自定义加载器加载类调用方法 ------
com.autumn.jvm.MyClassLoaderTest$MyClassLoader
====== load User ======
------ 另一个版本的User1:自定义加载器加载类调用方法 ------
com.autumn.jvm.MyClassLoaderTest$MyClassLoader
同一个 JVM 内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样。 因此要判断是否为同一个类对象的话,除了看类的包名和类名是否相同外,还需要看他们的类加载器是否为同一个。