JVM的类加载器

JVM的类加载器类加载器(ClassLoader)是JVM的核心组件之一,负责将.class文件加载到JVM,但从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

类加载器的层次结构

Bootstrap ClassLoader(启动类加载器)

  • 最顶层的类加载器

  • 负责加载JAVA_HOME/lib目录下的核心类库

  • 由C++实现,是JVM的一部分

Extension ClassLoader(扩展类加载器)

  • 负责加载JAVA_HOME/lib/ext目录下的扩展类库

  • 由Java实现,是sun.misc.Launcher$ExtClassLoader类

Application ClassLoader(应用程序类加载器)

  • 也称为System ClassLoader

  • 负责加载用户类路径(ClassPath)上的类库

  • 由Java实现,是sun.misc.Launcher$AppClassLoader类


双亲委派模式

双亲委派模式,可以用一句话来说表达:**任何一个类加载器在接到一个类的加载请求时,都会先让其父类进行加载,只有父类无法加载(或者没有父类)的情况下,才尝试自己加载,**双亲委派模型的工作流程如下:

  1. 当一个类加载器收到类加载请求时,首先不会自己尝试加载,而是委托给父类加载器

  2. 父类加载器会继续向上委托,直到启动类加载器

  3. 如果父类加载器无法完成加载,子加载器才会尝试自己加载

其实JVM 对类的唯一标识,可以简单的理解为由ClassLoader id + PackageName + ClassName组成,因此在一个运行程序中有可能存在两个包名和类名完全一致的类,但是如果这两个类不是由一个 ClassLoader 加载,会被视为两个不同的类,此时就无法将一个类的实例强转为另外一个类,这就是类加载器的隔离性。

自定义类加载器

我们可以通过继承ClassLoader类来实现自定义的类加载器。通常只需要重写findClass方法:

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
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String classPath;

public MyClassLoader(String classPath) {
this.classPath = classPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}

private byte[] getClassData(String className) {
String path = classPath + File.separatorChar +
className.replace('.', File.separatorChar) + ".class";
try (InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

public static void main(String[] args) throws Exception {
// 使用自定义类加载器加载类
MyClassLoader classLoader = new MyClassLoader("/path/to/classes");
Class<?> clazz = classLoader.loadClass("com.example.Test");
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
}
}

类加载过程

类加载过程分为以下三个阶段:

加载(Loading)

  • 通过类的全限定名获取类的二进制字节流

  • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构

  • 在内存中生成一个代表该类的Class对象,作为方法区这个类的各种数据的访问入口

链接(Linking)

  • 验证(Verification): 确保Class文件的字节流符合JVM规范

  • 准备(Preparation): 为类变量分配内存并设置初始值

  • 解析(Resolution): 将符号引用转换为直接引用

初始化(Initialization)

  • 执行类构造器<clinit>()方法,为类变量赋正确的初始值

打破双亲委派模型

在某些场景下需要打破双亲委派模型,例如热部署、OSGi框架、Tomcat等Web容器,打破双亲委派的示例(参考互联网CSDN):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BreakDelegationClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 对于指定的类,使用自定义加载方式
if (name.startsWith("com.example.break")) {
return findClass(name);
}
// 其他类仍然遵循双亲委派
return super.loadClass(name);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 自定义类加载逻辑
byte[] classData = getClassData(name); // 实现同前例
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}
}

类加载器的应用场景

模块化与热部署

  • 每个模块使用独立的类加载器

  • 修改模块后可以重新加载而不影响其他模块

代码加密

  • 对class文件加密,自定义类加载器解密

实现不同版本类库共存

  • 不同类加载器加载不同版本的类

Android中的DexClassLoader

  • 动态加载dex文件

常见面试问题QA

Q:如何判断两个类是否相同?

A:在JVM中,两个类是否相同不仅取决于类名是否相同,还取决于加载它们的类加载器是否相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ClassIdentityTest {
public static void main(String[] args) throws Exception {
String classPath = "/path/to/classes";
MyClassLoader loader1 = new MyClassLoader(classPath);
MyClassLoader loader2 = new MyClassLoader(classPath);

Class<?> clazz1 = loader1.loadClass("com.example.Test");
Class<?> clazz2 = loader2.loadClass("com.example.Test");

System.out.println(clazz1 == clazz2); // 输出false
System.out.println(clazz1.getClassLoader()); // 输出loader1
System.out.println(clazz2.getClassLoader()); // 输出loader2
}
}