0%

jvm 类加载过程

本文介绍 JVM 的类加载过程,以及类加载器

什么是类加载

java 的源码编译为字节码 Class 文件,需要加载到 JVM 中才能运行,类加载过程就是如何将 Class 文件加载到 JVM 中的过程

注:

  1. 不要和对象的实例化搞混,这个过程不涉及创建对象
  2. 只有第一次使用这个类时才会加载

加载流程

类加载过程.drawio
  1. loading(加载):

    1. 将 class 文件(二进制数据)写入内存
    2. 转化为 jvm 能识别的数据结构
    3. 在内存中生成代表该类的 java.lang.Class 对象,作为方法区访问该类的入口
  2. Linking(链接):将内存中的 class 文件合并至 JVM 中,分为验证、准备、解析三个阶段

    1. Verification(验证):对 class 文件进行合法校验,如:字节码格式、语法规范、引用验证

    2. Preparation(准备):为类的静态变量分配内存(在方法区中),此时静态变量的值为默认值,如static int A = 9;这一步会为 A 在方法区中申请 4 字节的空间,由于该空间的二进制位全 0,所以此时 A = 0

      注:

      1. 如果是public final static int A = 9;则此时 A = 9,这是因为 static final 修饰的变量在编译期间会生成 ConstantValue 属性,在类加载的准备阶段根据 ConstantValue 的值为该字段赋值

        static final 变量没有默认值,必须显式地赋值,否则编译时会报错

      2. 1.8之后,字符串常量放到堆空间中

    3. Resolution(解析):将常量池中的符号引用解析为直接引用(目标的指针、偏移量、句柄)

  3. Initializing(初始化):静态变量赋初始值,如static int A = 9;这一步会将 A 赋为 9

类加载器

上面介绍了一个类加载到 JVM 内存的过程,类加载器执行其中的 loading 过程

分类

  1. Bootstrap
  2. Extension
  3. App
  4. Custom ClassLoader

每个类加载器都有一块缓存,保存自己加载过的类

类加载器.drawio

双亲委派

工作原理

  1. 一个类加载器收到了类加载的请求,先到自己的缓存中查找是否加载过
  2. 如果缓存中存在,则返回加载的类;如果缓存中不存在,交给父加载器
  3. 如果父加载器的缓存中存在,则返回加载到JVM内存的地址;如果没有,继续交给父加载器,直到 BootStrap
  4. 如果 BootStrap 的缓存中也不存在,开始从负责的范围中寻找
  5. 如果 BootStrap 负责的范围内找不到类的 Class 文件,依次交给子加载器
  6. 如果某个加载器在负责的范围内找到了要加载的类,写入自己的缓存区,并返回地址

上述步骤,1-3 为自下而上,4-6 为自上而下

为什么要有双亲委派来加载类?

安全性:如果核心类如 java.lang.String 可以由开发者自定义的类加载器加载,则开发者可以覆盖该类型,在里面执行恶意代码。如果使用双亲委派,自定义类加载器的缓存区中没有 java.lang.String 类型,只能交给上层加载器,从而避免了这个问题

源码分析

类加载源码

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
// java.lang.ClassLoader#loadClass(java.lang.String, boolean)
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);
// 缓存中没有,交给父加载器或Bootstrap
if (c == null) {
long t0 = System.nanoTime();
try {
// 有父类,交给父类
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 没有父类,说明当前是 bootstrap,自己加载
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;
}
}

查看一个类的加载器

1
System.out.println("String ClassLoader: " + String.class.getClassLoader());j

自定义类加载器

为什么需要自定义类加载器

举几个自定义类加载器的应用场景:

  1. 源码加密:Java 代码可以被反编译,如果需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用 JVM 的 ClassLoader 去加载类了,这时就需要自定义 ClassLoader 在加载类的时候先解密,然后再加载
  2. 从非标准源加载代码:如果你的字节码是放在数据库、某个服务器,就可以自定义类加载器,从指定的来源加载类
  3. 防止多个项目间出现重名的类:比如 Tomcat 需要部署多个项目,无法保证不同项目之间没有重名(package + 类名)的类,如果只有用一个加载器,无法同时加载多个重名的类(见下文例子)
  4. 热加载(Hot Swap),热加载需要重新加载类,由于系统级别的加载器不能手动创建新的加载器对象,只要类加载过,就只能从缓存中读,不能清除缓存,无法重新加载,但是自定义的类加载器可以随意 new 新的,从而重新加载覆盖原有的类对象

热加载是针对单个字节码文件,重新编译后,不需要重启进程,应用程序就可以使用新的class 文件

源来:Class Loaders in Java JVM——自定义类加载器 What is the use of Custom Class Loader

如何自定义类加载器

继承 ClassLoader,重写 findClass,不建议重写 loadClass,因为重写 loadClass 如果打破了双亲委派,当要加载的类依赖于 java.lang.String 等需要父加载器加载的类,则会因无法加载而报错

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
package org.example;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class CustomerClassLoader extends ClassLoader {
// 类源文件编译产物的路径
private String classFilePath;

public customerClassLoader() {}

public customerClassLoader(String classFilePath) {
this.classFilePath = classFilePath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = getClassBytes(this.classFilePath);
Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
return c;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

// 读取 .Class 文件的二进制内容
private byte[] getClassBytes(String filePath) throws IOException {
File file = new File(filePath);
FileInputStream inputStream = new FileInputStream(file);
FileChannel channel = inputStream.getChannel();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
WritableByteChannel writableByteChannel = Channels.newChannel(outputStream);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true) {
int i = channel.read(byteBuffer);
if (i == 0 || i == -1) {
break;
}
byteBuffer.flip();
writableByteChannel.write(byteBuffer);
byteBuffer.clear();
}
inputStream.close();
return outputStream.toByteArray();
}

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
CustomerClassLoader customerClassLoader =
new customerClassLoader("/Users/zhangjiaming/Desktop/consumeClassLoader/People.class");
Class<?> clazz = customerClassLoader.loadClass("People");
// Class<?> clazz = Class.forName("People", true, customerClassLoader);
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
Object obj = constructor.newInstance("Bob");
System.out.println(obj);
System.out.println(obj.getClass().getClassLoader());
}

}
image-20230108134131391

自定义类加载器的应用

加载多个同名类
应用场景

比如一个方法有多个版本,我们希望同时使用多个版本的类;Tomcat 中运行了多个项目,项目中存在重名的类

一个类加载器为什么不能加载多个同名类?

因为类加载器 L 会先到缓存中查找是否已加载,对于同名类(package + 类名),当 A 项目中的 com.model.Dog.java 已经加载了,L 就不会再去加载 B 项目中的 com.model.Dog.java,这样 B 项目用了错误的类,导致 B 项目运行报错

验证

准备两个 java 文件,放到 A,B两个路径下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.zjm.model;
public class People {
private String name;

public People(String name) {
this.name = name;
}

// 第一个文件
public String toString() {
return "I am a A people, my name is " + name;
}
// 第二个文件
public String toString() {
return "I am a B people, my name is " + name;
}

}

自定义类加载器用上文的,修改 main 函数,令一个类加载器加载

1
2
3
4
5
6
7
8
9
10
11
12
13
private static void test(String path, String className, CustomerClassLoader customerClassLoader) throws Exception {
customerClassLoader.setClassFilePath(path);
Class<?> clazz = customerClassLoader.loadClass(className);
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
Object obj = constructor.newInstance("Bob");
System.out.println(obj);
}

public static void main(String[] args) throws Exception {
CustomerClassLoader customerClassLoader = new CustomerClassLoader();
test("/Users/zhangjiaming/Desktop/cunstomerClassLoader/A/People.class", "com.zjm.model.People", customerClassLoader);
test("/Users/zhangjiaming/Desktop/cunstomerClassLoader/B/People.class", "com.zjm.model.People", customerClassLoader);
}
image-20230110140323056

可以发现,第二次没有加载新的类,而是使用了已加载的类,所以要使用不同的类加载器,如下:

1
2
3
4
5
6
public static void main(String[] args) throws Exception {
ConsumerClassLoader consumerClassLoader = new ConsumerClassLoader();
test("/Users/zhangjiaming/Desktop/consumeClassLoader/A/People.class", consumerClassLoader);
ConsumerClassLoader consumerClassLoader1 = new ConsumerClassLoader();
test("/Users/zhangjiaming/Desktop/consumeClassLoader/B/People.class", consumerClassLoader1);
}
image-20230110140449779
热加载
应用场景

修改代码并编译后,不需要重启服务,直接生效

实现思路

监听 Class 文件变化,当 Class 文件变化时,创建新的类加载器加载类,覆盖原来的 Class 对象

验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void test(String path) throws Exception {
Class<?> clazz = null;
for (int i = 0; i < 1000; i++) {
ConsumerClassLoader consumerClassLoader = new ConsumerClassLoader();
consumerClassLoader.setClassFilePath(path);
clazz = consumerClassLoader.loadClass("com.zjm.model.People");
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
Object obj = constructor.newInstance("Bob");
System.out.println(obj);
Thread.sleep(1000);
}
}
public static void main(String[] args) throws Exception {
test("/Users/zhangjiaming/Desktop/consumeClassLoader/A/People.class");
}
image-20230110192624216

这里只是一个简单的例子,要实现热加载还是有很多点要考虑,待后续讨论

字节码加密

通过某种加密算法修改 Class 文件,然后通过解密算法解密,再加载

参考文献