Java的类加载器是Java运行时环境中的重要组件,核心功能是将类的字节码加载到Java虚拟机中。
举个例子
可以通过一个图书馆的比喻来形象地解释类加载器的作用、用法和使用场景。
想象一下,有一个巨大的图书馆(JVM),其中有非常多的藏书(类)。当你(程序)需要阅读一本书(使用一个类)时,你首先需要找到这本书。这就是类加载器的职责:找到这些类的字节码,并将它们带到图书馆(JVM)中,使其可以被使用。
类加载器的分类
在 Java 中,类加载器大体可以分为三种:
- 引导类加载器(Bootstrap ClassLoader):
这是最顶层的加载器,负责加载 Java 核心库(如java.lang.*
等)。你可以把它看作是图书馆的基础藏书区,这里存放的都是一些最基本、最常用的书籍。 - 扩展类加载器(Extension ClassLoader):
它负责加载 Java 扩展库,位于jre/lib/ext
目录或者由 java 系统属性java.ext.dirs
指定路径中的类库。想象这是图书馆的特殊资源区,比基础藏书区的书籍更专业,但不是每个人都需要。 - 应用程序类加载器(Application ClassLoader):
这是加载你自己的代码和你使用的第三方库的类加载器。可以视作图书馆的普通藏书区,这里放着大量由各种出版社出版的各类书籍。
代码示例
理论差不多了,下面用代码对类加载器进行展开。
- 这是获取类加载器的一段代码
public class ClassLoaderExp {
public static void main(String[] args) {
ClassLoader classLoader = ClassLoaderExp.class.getClassLoader();
System.out.println(classLoader);
}
}
输出结果是 jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
,这个结果是 Java 类加载器实例的一个默认 toString()
方法的结果(备注,此代码是运行在JDK17上,如果采用其他版本JDK可能会有区别)。这里面包含了几部分信息:
(1)类加载器的类型:jdk.internal.loader.ClassLoaders$AppClassLoader
指示这是 Java 内部使用的应用程序类加载器(Application ClassLoader),它是用来加载你的应用程序中的类(包括第三方库)的。
(2)实例标识符:@63947c6b
是这个特定实例的哈希码,这是 JVM 在运行时为对象分配的一个标识符。
2. 我们也可以查看系统自带的类的类加载器
public class ClassLoaderExp {
public static void main(String[] args) {
ClassLoader classLoader = Integer.class.getClassLoader();
System.out.println(classLoader);
}
}
输出结果是 null
,这是因为引导类加载器是C++写的,所以虽然输出null,但是可以默认它是引导类加载器(Bootstrap ClassLoader)。
3. 我们也可以自定义类加载器,可以从指定的文件路径加载类,这在需要从非标准源加载类(网络、加密文件等)时很有用。
public class CustomClassLoader extends ClassLoader {
@Override
// findaClass是用于查找类的方法,如果不重写这个方法,那么默认会调用父类的findClass方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassFromFile(name); // loadClassFromFile 方法是自定义的,用于从文件中读取类的字节码
/**
* defineClass() 方法是 ClassLoader 类的一个非常重要的方法,它用于将字节码数组转换成 Class 类的一个实例
* name 是类的全限定名,如 com.example.MyClass
* classData 是类的字节码数组
* 0 是字节码数组的起始位置
* classData.length 是字节码数组的长度
*/
return defineClass(name, classData, 0, classData.length);
}
/**
* 根据类的名字加载类文件的字节码
* File.separatorChar是文件分隔符
* 这是因为在文件系统中,Java 的全类名(如 com.example.MyClass)需要转换为目录结构的路径(如 com/example/MyClass)
*/
private byte[] loadClassFromFile(String fileName) {
InputStream resourceAsStream = getClass()
.getClassLoader()
.getResourceAsStream(fileName.replace('.', File.separatorChar) + ".class");
byte[] buffer;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int nextValue = 0;
try {
while ((nextValue = resourceAsStream.read()) != -1) {
byteArrayOutputStream.write(nextValue);
}
} catch (Exception e) {
e.printStackTrace();
}
buffer = byteArrayOutputStream.toByteArray();
return buffer;
}
}
使用代码:
public class ClassLoaderExp {
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
Class<?> clazz = customClassLoader.loadClass("com.example.proxy2.Car"); // 这是随便写的一个类
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
类加载器的使用场景
- 隔离加载: 在大型应用中,可能需要加载来自不同来源的相同名称的类,这时候类加载器可以通过不同的命名空间来实现类的隔离,防止命名冲突。就像图书馆中,可能有几本同名的书,但它们的作者或版本不同。
- 热替换和代码热部署: 在服务器运行时,你可能想要更新或替换某些类而不停机。使用自定义的类加载器可以在不重启应用的情况下重新加载修改过的类,就像图书馆在不关闭的情况下,替换掉某些旧书籍或者加入新书。
- 加密和解密: 出于安全考虑,可以使用自定义的类加载器来对类的字节码进行加密。当使用这些类时,通过类加载器对其进行解密后加载到 JVM 中,确保字节码在磁盘上的安全,类似于对图书馆中某些珍贵书籍进行加密保护。