Java 类加载器(Class Loader)是 Java 运行时环境的一部分,它负责在应用程序运行时加载类和接口的字节码。类加载器对于 Java 的动态特性和安全性有着至关重要的作用。下面将详细介绍 Java 类加载器的基本概念、层次结构以及类的加载过程。
基本概念
-
类加载:指的是将类的
.class
文件中的二进制数据读入到内存中,然后对其数据进行校验、转换解析并生成方法区中的运行时数据结构,最后在堆区创建一个java.lang.Class
对象的过程。 -
类加载器分类:
- 启动类加载器(Bootstrap ClassLoader):是最顶层的类加载器,负责加载
<JAVA_HOME>\lib
目录中的或者被-Xbootclasspath
参数指定路径中的,并且能够被虚拟机识别的类(如java.lang.Object
)。此加载器无法被 Java 程序直接获取到。 - 扩展类加载器(Extension ClassLoader):这个类加载器由
sun.misc.Launcher$ExtClassLoader
实现,它负责加载<JAVA_HOME>\lib\ext
目录中的,或者被-Djava.ext.dirs
系统变量所指定的路径中的所有类库。 - 应用类加载器(Application ClassLoader):也称为系统类加载器,由
sun.misc.Launcher$AppClassLoader
实现。它负责加载用户类路径(ClassPath)上所指定的类。可以通过ClassLoader.getSystemClassLoader()
获取到该类加载器。
- 启动类加载器(Bootstrap ClassLoader):是最顶层的类加载器,负责加载
类加载器的层次结构
Java 类加载器具有层次结构,每个类加载器都有一个父加载器。如果一个类加载器收到了加载类的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父类加载器去完成,每一层的类加载器都是基于这种“父母委托模型”来工作的。
类的加载过程
类的加载过程可以分为三个阶段:加载(Loading)、链接(Linking)和初始化(Initialization)。
-
加载(Loading):通过类的全限定名获取定义此类的二进制流;将此二进制流所代表的静态存储结构转化为方法区中的运行时数据结构;在 Java 堆中生成一个代表这个类的
java.lang.Class
对象,作为方法区这些数据的访问入口。 -
链接(Linking):验证(Verification),确保类文件的字节流包含的信息符合当前虚拟机的要求;准备(Preparation),为类变量分配内存并设置类变量初始值;解析(Resolution),将常量池内的符号引用替换为直接引用。
-
初始化(Initialization):执行类构造器
<clinit>()
方法,对类变量赋予正确的初始值。
双亲委派模型
双亲委派模型要求除了顶层的 Bootstrap ClassLoader 之外,其余的类加载器都应该有自己的父类加载器。这里的类加载器之间的父子关系一般不会反映到它们所在的 Java 类的继承层次关系中,因为类加载器类层次中的“父类加载器”仅仅是一个命名上的习惯。
双亲委派模型保证了 Java 核心 API 包的唯一性,防止了用户自定义的类加载器加载这些核心类库。
自定义类加载器
在 Java 中,自定义类加载器是通过继承 java.lang.ClassLoader
类并重写其 findClass
方法来实现的。ClassLoader
是所有类加载器的基类,它提供了类加载器的基本功能和服务。自定义类加载器通常用于实现更灵活的类加载策略,比如从特定位置加载类,或是实现类的热更新等需求。
下面是一个简单的自定义类加载器示例,展示如何实现基本的类加载逻辑:
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = name.replaceAll("\\.", "/");
InputStream is = getClass().getResourceAsStream(classPath + "/" + path + ".class");
if (is == null) {
throw new ClassNotFoundException("Class not found: " + name);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
try {
while ((bytesRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
is.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
byte[] b = baos.toByteArray();
return defineClass(name, b, 0, b.length);
}
}
在这个例子中,CustomClassLoader
继承自 ClassLoader
,并且重写了 findClass
方法。当 JVM 请求 CustomClassLoader
加载一个类时,它会查找类文件并将其转换为字节数组,然后使用 defineClass
方法将这些字节定义成一个 Class
对象。
为了使用这个自定义类加载器,你需要实例化它,并使用它的 loadClass
方法来加载类,如下所示:
public class Main {
public static void main(String[] args) throws Exception {
CustomClassLoader loader = new CustomClassLoader("/path/to/classes");
Class<?> myClass = loader.loadClass("com.example.MyClass");
Object instance = myClass.newInstance();
// 使用 instance 进行进一步的操作
}
}
在这个例子中,/path/to/classes
是你的类文件所在目录的路径,com.example.MyClass
是你要加载的类的完全限定名。
注意事项
- 类加载器隔离性:每个类加载器加载的类都与其他类加载器加载的同名类相互独立,这意味着即使两个类加载器加载相同的类文件,这两个类在 JVM 中也被视为不同的类。
- 资源定位:自定义类加载器需要正确地定位类文件的位置,并且要处理好类路径的问题。
- 安全性和权限:自定义类加载器可能会涉及到安全性和权限的问题,例如,某些操作可能需要特定的安全管理器权限。
通过自定义类加载器,你可以实现许多高级特性,如代码热插拔、模块化加载、安全沙箱等。然而,编写自定义类加载器需要仔细考虑与 JVM 类加载机制相关的各种细节,包括类加载顺序、类的可见性以及安全性等问题。