# Java类加载机制
Java类加载机制(Class Loading Mechanism)是Java虚拟机(JVM)将Java类的字节码文件(.class
文件)加载到内存中的过程,并为其创建对应的Class对象。类加载机制是Java运行时的核心功能之一,确保类在被使用之前被正确加载、连接和初始化。
Java的类加载机制采用了动态加载(按需加载)和双亲委派模型来管理类的加载顺序和安全性。
# 一、Java类加载的基本流程
Java的类加载过程分为以下几个阶段:
- 加载(Loading)
- 连接(Linking)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
# 1. 加载(Loading)
在类加载阶段,JVM会通过类加载器找到 .class
文件,并将它的字节码读入内存,生成对应的 Class
对象。这一步主要完成以下操作:
- 通过类的全限定名(如
com.example.MyClass
)来定位该类的字节码文件。 - 使用类加载器读取字节码文件,并生成
Class
对象。 - 将类的静态数据结构存储在内存的方法区中。
# 2. 连接(Linking)
连接阶段是将类的二进制数据合并到 JVM 的运行时环境中,分为三个子阶段:
- 验证(Verification):确保类的字节码符合JVM的规范,防止恶意字节码攻击。
- 准备(Preparation):为类的静态变量分配内存,并将它们初始化为默认值(如
int
类型的默认值为0
,引用类型为null
)。 - 解析(Resolution):将类的符号引用(常量池中的符号)替换为直接引用(具体内存地址)。这一步是将符号链接解析为可以被程序直接访问的内存地址。
# 3. 初始化(Initialization)
初始化阶段是类加载过程的最后阶段,它会执行类的静态初始化块(static
块)和为静态变量赋予初始值。这一步是在类首次主动使用时进行的。
注意:类在初始化时才会执行其静态块和静态变量的初始化,加载阶段仅仅是将类信息加载到内存中,不会执行任何代码。
# 二、双亲委派模型
Java的类加载机制遵循双亲委派模型(Parent Delegation Model),即每个类加载器在加载一个类时,首先会把类加载请求委托给其父加载器。如果父加载器无法完成类的加载任务,才会尝试自己加载。
# 双亲委派的基本流程:
- 类加载器接收到类加载请求。
- 将加载请求委派给父类加载器,递归向上,直至根类加载器。
- 如果父类加载器能够加载该类,则加载成功;否则由当前类加载器进行加载。
# 类加载器的分类:
- Bootstrap ClassLoader(引导类加载器):负责加载
<JAVA_HOME>/lib
目录下的核心类库,如rt.jar
。这是最顶层的类加载器,通常由C++实现,不属于java.lang.ClassLoader
的子类。 - Extension ClassLoader(扩展类加载器):负责加载扩展目录下的类库,如
<JAVA_HOME>/lib/ext
目录中的类。 - Application ClassLoader(应用类加载器):负责加载用户类路径(CLASSPATH)下的类。这个加载器是最常用的,加载应用程序的类。
# 双亲委派模型的优点:
- 安全性:通过双亲委派,核心类库只能由引导类加载器加载,防止应用程序类加载器加载核心类并篡改。
- 避免重复加载:类加载器总是先委托给父类加载器,避免同一个类被加载多次,确保类的唯一性。
# 三、类加载器的分类
Java中类加载器分为以下几种类型:
# 1. 启动类加载器(Bootstrap ClassLoader)
- 这是最顶层的类加载器,负责加载Java核心类库(例如
rt.jar
)。 - 它是由JVM自带的,并不是
ClassLoader
的子类,通常由本地代码(C/C++)实现。
# 2. 扩展类加载器(Extension ClassLoader)
- 负责加载
<JAVA_HOME>/lib/ext
目录下的类库,或者由系统变量java.ext.dirs
指定的路径中的类。 - 它是
ClassLoader
类的子类。
# 3. 应用程序类加载器(Application ClassLoader)
- 负责加载应用程序的类路径(
CLASSPATH
)中的类文件。 - 它也是
ClassLoader
类的子类,是我们开发中最常接触的类加载器。
# 4. 自定义类加载器
- 用户可以通过继承
ClassLoader
类并重写findClass()
方法,来实现自定义类加载器。自定义类加载器可以加载远程的类、加密的类文件等。
# 四、类加载的时机
类的加载并不是立即发生的,而是按需进行。当且仅当类首次主动使用时,才会触发类的加载和初始化。
以下几种情况会触发类的初始化:
- 创建类的实例(如
new
操作)。 - 访问类的静态变量或调用静态方法。
- 反射操作(如
Class.forName()
)。 - 初始化子类时,父类也会被初始化。
- Java虚拟机启动时会先初始化主类(包含
main
方法的类)。
# 五、类的加载顺序
- 父类的静态块先于子类的静态块执行。
- 父类的静态变量先于子类的静态变量初始化。
- 父类的实例变量先于子类的实例变量初始化。
- 父类的构造方法先于子类的构造方法执行。
# 六、类加载的特点
- 按需加载:Java类是在首次使用时才会加载,而不是在程序启动时全部加载。
- 一次加载:类只会加载一次,在JVM中形成唯一的Class对象,避免重复加载同一个类。
# 七、类卸载
类的卸载发生在类加载器不再使用,且该类的所有对象都不可达时。Java类的卸载非常罕见,通常JVM不会主动卸载类。通过自定义类加载器和热部署技术,可以在应用服务器中实现类的动态卸载。
# 八、示例:自定义类加载器
下面是一个自定义类加载器的简单实现,通过它可以加载指定目录下的类文件:
import java.io.*;
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) throws ClassNotFoundException {
String fileName = classPath + className.replace(".", "/") + ".class";
try {
InputStream inputStream = new FileInputStream(fileName);
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue = 0;
while ((nextValue = inputStream.read()) != -1) {
byteStream.write(nextValue);
}
return byteStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("Class not found in path: " + classPath);
}
}
}
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
# 总结
Java的类加载机制通过双亲委派模型和类加载器实现了按需加载、类隔离和安全性保障。了解类加载的流程和原理可以帮助开发者调试类加载问题(如类冲突、重复加载),并通过自定义类加载器来实现动态加载、插件化开发等高级功能。