Java类加载器可以使得 Java 类可以被动态加载到 Java 虚拟机中并执行。
类加载器的基本概念
类加载器就是用来加载Java类到Java虚拟机中。
ClassLoader类介绍
ClassLoader 的基本职责是根据一个类的名称,找到或者生成对应的字节码,然后根据字节码定义一个Java类,即Class类的实例。
ClassLoader 中与加载类相关的方法
方法 | 说明 |
---|---|
getParent() | 返回该类加载器的父类加载器。 |
loadClass(String name) | 加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findClass(String name) | 查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findLoadedClass(String name) | 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的。 |
resolveClass(Class<?> c) | 链接指定的 Java 类。 |
类加载器的树状结构
- 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader 。
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。
触引导类加载器,其余加载器都有一个父类加载器。我们自己定义的类加载器,其父类加载器是加载此类加载器 Java 类的类加载器。
类加载器的代理模式
就是所说的双亲委派机制,当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
Java虚拟机是怎么判断两个类是否相同?
Java虚拟机不仅要看两个类的全路径是否相同,还要看加载该类的类加载器是否一样。即使是一个类,被不同的类加载器加载,其Class是不同的。例如Sample.class被ClassLoaderA 和ClassLoaderB加载,将加载后的两个类进行赋值,会抛出ClassCastException 。
代理莫的用处:
- 保证了核心类库的类型安全。所有的Java应用都至少引用Object,如果Object被不同的类加载器加载到Java虚拟机中,这样会导致多个版本的Object类,而这些类之间还不兼容。
- 不同的类加载器为相同名称的类创建了额外的名称空间。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。
加载类的过程
由于类加载器的代理模式,一个类的加载过程中,启动这个过程的类加载器和真正对这个类的加载,可能不是同一个类加载器。Java虚拟机判断两个类是否相同,是通过类定义加载器。
真正完成类加载的工作是通过调用defineClass来实现,被称为类的定义加载器(defining loader)。
启动类的加载过程是通过loadClass来实现,被称为初始加载器(initiating loader)。
两种类加载器的关联之处是:一个类的加载器是他所引用的类的初始化加载器。例如Outer 引用Inner ,则Outer 的定义加载器负责启动Inner的加载过程。
loadClass() 抛出的是 java.lang.ClassNotFoundException 异常。defineClass() 抛出的是 java.lang.NoClassDefFoundError 异常。
类加载器在成功加载某个类后,会把java.lang.Class实例缓存起来。下次在调用加载的时候,类加载器会直接使用缓存中的类实例,而不会再去加载。也就是相同的全类名,一个类加载器只会加载一次。
线程上下文类加载器
通过Thread的getContextClassLoader()和setContextClassLoader()来获取和设置线程的上下文类加载器。如果没有进行设置线程的上下文类加载器的话,将继承父线程的上下文类加载器。
Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
而线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。
Class.forName
- Class.forName(String name, boolean initialize, ClassLoader loader)
- Class.forName(String className)
name 表示的是类的全名; initialize 表示是否初始化类;loader 表示加载时使用的类加载器。
开发自己的类加载器
文件系统类加载器
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
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文件
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
try (InputStream is = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream();) {
byte[] buffer = new byte[1024];
int bytesNumRead = 0;
while ((bytesNumRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
}
} catch (IOException e) {
return null;
}
}
// 类路径转文件路径
private String classNameToPath(String className) {
String separator = Matcher.quoteReplacement(File.separator);
// 注意 . 需要转义 , windows 下的路径分隔符同样需要转义
return rootDir + separator + className.replaceAll("\\.", separator) + ".class";
// 或者使用下边的方式
//return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
}
}
类加载器与web容器
通常每个web应用对应一个类加载器,该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。