THIS IS B3c0me

记录生活中的点点滴滴

0%

Java类加载器

一、类加载器简介

Java类加载器(Class Loader)是Java虚拟机(JVM)的一部分,负责将类的字节码加载到内存中,并将其转换为可执行的Java对象。类加载器在Java应用程序中起着重要的作用,它实现了动态加载类的机制,使得Java具备了灵活性和可扩展性。

类加载器是Java虚拟机用于加载类文件的一种机制。在Java中,每个类都由类加载器加载,并在运行时被创建为一个Class对象。类加载器负责从文件系统、网络或其他来源中加载类的字节码,并将其转换为可执行的Java对象。类加载器还负责解析类的依赖关系,即加载所需的其他类。

二、类加载的过程

类加载器的工作可以简化为三个步骤:

  • 加载(Loading):根据类的全限定名(包括包路径和类名),定位并读取类文件的字节码。

  • 链接(Linking):将类的字节码转换为可以在虚拟机中运行的格式。链接过程包括三个阶段:

    • 验证(Verification):验证字节码的正确性和安全性,确保它符合Java虚拟机的规范。
  • 准备(Preparation):为类的静态变量分配内存,并设置默认的初始值
  • 解析(Resolution):将类的符号引用(比如方法和字段的引用)解析为直接引用(内存地址)
  • 初始化(Initialization):执行类的初始化代码,包括静态变量的赋值和静态块的执行。

三、类加载器的分类

主要分为两类:

  1. JVM内置的类加载器,有Bootstrap加载器、ExtClassLoader加载器和AppClassLoader加载器 三种,分别负责加载不同目录下的.class文件

  2. 用户自定义的类加载器,负责的加载目录自己决定。

3.1 引导类加载器Bootstrap加载器

引导类加载器属于JVM的一部分,由C++代码实现。

引导类加载器负责加载<JAVA_HOME\>\jre\lib路径下的核心类库,由于安全考虑只加载包名 java、javax、sun开头的类。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BootStrap {
public static void main(String[] args) {
//Bootstrap 引导类加载器
//打印为null,是因为Bootstrap是C++实现的。
ClassLoader classLoader = Object.class.getClassLoader();
System.out.println(classLoader);
//查看引导类加载器会加载哪些jar包
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for(URL u : urls){
System.out.println(u);
}
}
}

运行结果

3.2 扩展类加载器ExtClassLoader

全类名:sun.misc.Launch$ExtClassLoader,Java语言实现。

扩展类加载器的父加载器是Bootstrap启动类加载器 (注:不是继承关系)

扩展类加载器负责加载<JAVA_HOME>\jre\lib\ext目录下的类库。

3.3 系统类加载器 AppClassLoader

全类名: sun.misc.Launcher$AppClassLoader

系统类加载器的父加载器是ExtClassLoader扩展类加载器(注: 不是继承关系)。

系统类加载器负责加载 classpath环境变量所指定的类库,是用户自定义类的默认类加载器。

示例

1
2
3
4
5
6
7
8
public static void main(String[] args) {
//获取系统类加载器
ClassLoader classLoader = BootStrap.class.getClassLoader();
System.out.println(classLoader);

ClassLoader classLoader1 = ClassLoader.getSystemClassLoader();
System.out.println(classLoader1);
}

运行结果

3.4 上三者之间的关系

  • AppClassLoader的父加载器是ExtClassLoader

  • ExtClassLoader的父加载器是Bootstrap

  • Bootstrap是根加载器

  • 三者之间没有继承关系

1
2
3
4
5
6
7
8
9
10
11
AppClassLoader和ExtClassLoader都实现了抽象类ClassLoader。

抽象类ClassLoader有一个字段parent, AppClassLoader和ExtClassLoader通过设置该字段引用,指定父加载器。(是组合关系)

AppClassLoader 的parent指向 ExtClassLoader
ExtClassLoader 的parent指向 null,(null的原因是因为Bootstrap是C++实现的,通过代码中逻辑判断来转向Bootstrap)

// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;

3.5 自定义类加载器

自定义类加载器是为了加载在jvm三个加载器负责的目录范围之外的类

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package classloader;

import java.io.*;
import java.sql.SQLOutput;

/**
* @Description : 自定义类加载器
* @Author: zcwww
* 2023/9/3 15:08
*/
public class MyClassLoader extends ClassLoader{
//url
private String classPath;

//构造器1
public MyClassLoader(String classPath){
this.classPath = classPath;
}

//指定父类加载器的构造器

public MyClassLoader(ClassLoader parent, String classPath){
/**

*@Desc: 指定父类加载器的构造器

*@Params: [parent:, classPath]

*@Retuen: null

*/
super(parent);
this.classPath = classPath;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
// //要求返回的是你要加载的字节码文件的Class对象.

//这里开始都是我们说了算的。
//步骤:
//1. 从本地或网络某处读一个输入流到内存中.
//2. 将流内容字节数组 封装成Class对象 (直接调ClassLoader的defineClass方法,JVM会帮我们按照.class文件格式创建好的。)

//1.
//处理得到完整路径
String path = this.classPath + name.replace(".", File.separator) + ".class";
//2.
//读取到内存
try (FileInputStream fis = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len = 0;
while ((len = fis.read(buffer)) != -1) {
//用ByteArrayOutputStream暂存一下。
baos.write(buffer, 0, len);
}
byte[] allByte = baos.toByteArray();
//将字节数组生成Class对象
return super.defineClass(name, allByte, 0, allByte.length);
} catch (IOException e) {
throw new ClassNotFoundException(name + "加载失败");
}
}
//测试
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
//使用自己的类加载器加载D:\\ com.ali.Hello
MyClassLoader myClassLoader = new MyClassLoader("D:\\");
Class<?> clazz = myClassLoader.loadClass("com.ali.Hello");
clazz.newInstance();
System.out.println(clazz.getClassLoader());
}

}

加载jar包的写法:从jar包加载类:

1
String path = "jar:file:\\" + classPath + "!/" + name.replace(".", File.separator) + ".class";

3.6 谁来准备类加载器

AppClassLoader和ExtClassLoader是Launcher的静态内部类,在程序启动时JVM会创建Launcher对象,Launcher构造器会同时会创建扩展类加载器和应用类加载器。

四、双亲委派机制

双亲委派机制就是: 每个类加载器都很懒,加载类时都先让父加载器去尝试加载,父加载器加载不了时自己才去加载。

例如: 加载自定义类Demo.class的流程

  • 首先使用AppClassLoader类加载器尝试加载,AppClassLoader加载器会先检查它的缓存,查看该类是否已经被加载,有则不加载,没有则向上交给ExtClassLoader加载器。
  • ExtClassLoader加载器同样会先检查它的缓存,查看该类是否已经被加载,有则不加载,没有则向上交给Bootstrap加载器。
  • Bootstrap加载器同样会先检查它的缓存,查看该类是否已经被加载。有则不加载,没有则尝试从它负责的目录中加载
  • Bootstrap加载器加载失败(不在它负责的目录范围)则向下交给ExtClassLoader加载器
  • ExtClassLoader加载器会从它负责的目录中尝试加载,加载失败则向下交给AppClassLoader加载器
  • AppClassLoader加载器从它负责的classpath尝试加载,加载完成。

4.1 双亲委派机制的好处

  1. 避免类的重复加载:当父加载器已经加载该类时,就没有必要子加载器再加载一遍,保证被加载类的唯一性。
  2. 同时Java有沙箱安全机制:自定义类的包名以 java.开头被禁止, 防止核心API被篡改,判断逻辑在defineClass方法中。

4.2 打破双亲委派机制

双亲委派机制的实现其实就是在loadClass方法中实现的。
直接调用findClass方法就可以跳过双亲委派机制,这样就可以直接加载,而不用向上委托了。

1
2
//find方法调用,加载 全限定名类
Class<?> clazz1 = myClassLoader1.findClass("com.ali.Hello");

五、ClassLoader抽象类

所有的类加载器(除了Bootstrap)都要继承ClassLoader抽象类。

方法源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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();
//真正将字节码文件加载到内存。
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;
}
}

六、URLClassLoader

java.net.URLClassLoader继承了ClassLoader类. 拓展了功能,能够从网络或本地加载类。默认的父加载器是AppClassLoader系统类加载器。

参考

1.https://blog.csdn.net/qq_21484461/article/details/131421264

2.https://blog.csdn.net/ZHHX666/article/details/124484199

欢迎关注我的其它发布渠道