类加载机制的深度解析

1. 类加载运行全过程

当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到 JVM。

有如下几步: 加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

  1. 加载:

    • 通过类的全限定名获取其二进制字节流:可以从本地文件系统、网络或其他来源获取字节码。
    • 将字节流转换为方法区中的对应的类结构。
    加载的方式
    • 启动类加载器(Bootstrap ClassLoader):负责加载核心类库(如 rt.jar 中的类)。
    • 扩展类加载器(Extension ClassLoader):负责加载扩展类库(如 jre/lib/ext 目录下的类)。
    • 应用程序类加载器(Application ClassLoader):负责加载用户类路径(classpath)中的类。
    • 自定义类加载器:可以通过继承 java.lang.ClassLoader 实现自定义加载逻辑。
  2. 验证:

    • 校验字节码文件的正确性且符合 JVM 规范,防止恶意代码破坏虚拟机的安全性。
    • 验证是一个耗时但必要的过程,确保了类的正确性和安全性。
  3. 准备:

    • 给类的静态变量分配内存,
    • 设置默认值:将静态变量初始化为其类型的零值(如 int 类型为 0boolean 类型为 false,引用类型为 null) 。(如 int 类型为 0boolean 类型为 false,引用类型为 null
    1
    2
    3
    public class Example {
    static int value = 123; // 静态变量
    }
    • 在准备阶段,value 被分配内存并初始化为 0
    • 初始化阶段才会将 value 设置为 123
  4. 解析:将符号引用替换为地址引用

    1
    2
    - 符号引用是以文本形式表示的目标地址或对象的逻辑名称。它并不直接指向内存中的实际位置,而是通过某种间接的方式(如名称、路径等)描述目标对象的位置
    - 直接引用是指向目标对象在内存中的实际地址。它是具体的物理地址,可以直接用于访问目标对象。
    1
    2
    3
    举例: 
    假设有一个类 `A` 中定义了一个方法 `void foo()`,在字节码文件中,这个方法可能以符号引用的形式表示为:Method #5; // NameAndType foo:()V, 这里的 `#5` 是一个索引,指向常量池中的某个条目,而 `NameAndType foo:()V` 表示方法的名字和签名。
    在运行时,JVM 将上述符号引用解析为实际的内存地址,例如:0x7f8000001234, 这个地址可以直接定位到方法 `foo()` 的代码段。
  5. 初始化

    • 对类的静态变量初始化为指定的值

    • 执行静态代码块

2. 类加载器

  1. **引导类加载器 (Bootstrap ClassLoader)**:
    • 职责:加载 Java 核心库中的类,如 java.lang.*java.util.* 等。
    • 加载路径:JVM 启动时指定的 rt.jar 或者 jmod 文件中的类。
    • 特点:由 JVM 实现,使用本地代码,不是一个普通的 Java 类。
  2. **扩展类加载器 (Extension ClassLoader)**:
    • 职责:加载标准扩展库中的类,位于 JAVA_HOME/lib/ext 目录或由系统属性 java.ext.dirs 指定的目录。
    • 加载路径lib/ext 目录中的 JAR 文件。
    • 特点:由 Java 实现,继承自 ClassLoader 类。
  3. **系统类加载器 (System ClassLoader) / 应用程序类加载器 (Application ClassLoader)**:
    • 职责:加载用户类路径 (classpath) 中的类,包含用户定义的类和第三方库。
    • 加载路径:通过命令行参数 -classpath 或环境变量 CLASSPATH 指定的路径。
    • 特点:默认的类加载器,可以通过 ClassLoader.getSystemClassLoader() 获取。
  4. **自定义类加载器 (Custom ClassLoader)**:
    • 职责:由用户定义,可以从任意位置加载类,例如网络、数据库等。
    • 加载路径:用户定义。
    • 特点:用户可以通过继承 ClassLoader 类并覆盖 findClass 方法来自定义类加载逻辑。

3. 双亲委派机制

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

image-20240603232254437

loadClass 方法的主要职责就是实现双亲委派机制:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载。

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
public abstract class ClassLoader {

//每个类加载器都有个父加载器
private final ClassLoader parent;

public Class<?> loadClass(String name) {

//查找一下这个类是不是已经加载过了
Class<?> c = findLoadedClass(name);

//如果没有加载过
if( c == null ){
//先委派给父加载器去加载,注意这是个递归调用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加载器为空,查找Bootstrap加载器是不是加载过了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加载器没加载成功,调用自己的findClass去加载
if (c == null) {
c = findClass(name);
}

return c;
}

protected Class<?> findClass(String name){
//1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
...

//2. 调用defineClass将字节数组转成Class对象
return defineClass(buf, off, len);
}

// 将字节码数组解析成一个Class对象,用native方法实现
protected final Class<?> defineClass(byte[] b, int off, int len){
...
}
}

3.1 作用

  1. 防止加载同一个.class。通过委托去询问上级是否已经加载过该.class,如果加载过了,则不需要重新加载。保证了数据安全。

  2. 保证核心.class不被篡改。通过委托的方式,保证核心.class不被篡改,即使被篡改也不会被加载,即使被加载也不会是同一个class对象,因为不同的加载器加载同一个.class也不是同一个Class对象。这样则保证了Class的执行安全。

3.2 如何打破双亲委派?

打破双亲委派,其实就是不走双亲委派那一套,而是走自定义的类加载器。

双亲委派的机制是ClassLoader中的loadClass方法实现的,打破双亲委派,其实就是重写这个方法,来用我们自己的方式来实现即可。典型的打破双亲委派模型的框架和中间件有tomcat。

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
public class CustomClassLoader extends ClassLoader {
private final String classPath;

public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 尝试加载自定义类
Class<?> clazz = findClass(name);
if (clazz != null) {
return clazz;
}
// 自己加载不了,再调用父类loadClass,保持双亲委托模式
return super.loadClass(name);
}

// 自定义类加载逻辑,例如从文件、网络等加载类字节码
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] result;
String path = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
try (FileInputStream fis = new FileInputStream(path)) {
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
result = buffer;
} catch (IOException e) {
throw new ClassNotFoundException("Could not load class " + name, e);
}
return defineClass(name, result, 0, result.length);
}
}

类加载机制的深度解析
http://example.com/类加载机制的深度解析/
作者
Panyurou
发布于
2024年7月14日
许可协议