类加载器简介
类加载器是负责加载类的对象。 它负责在运行时将 Java 类动态加载到 JVM(Java 虚拟机)。此外,它们是JRE(Java 运行时环境)的一部分。因此,由于类加载器,JVM不需要知道底层文件或文件系统以运行 Java 程序。
ClassLoader
类是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。
此外,Java 类不会同时加载到内存中,但是在应用程序需要时。这就是类加载器的用武之地。他们负责将类加载到内存中。
每个 Class 对象都包含对定义它的 ClassLoader 的引用。
数组类的类对象不是由类加载器创建的,而是根据 Java 运行时的需要自动创建的。Class.getClassLoader() 返回的数组类的类加载器与其元素类型的类加载器相同;如果元素类型是基本类型,则数组类没有类加载器。
应用程序实现 ClassLoader 的子类,以便扩展 Java 虚拟机动态加载类的方式。
安全管理器通常可以使用类加载器来指示安全域。
ClassLoader 类使用委派模型来搜索类和资源。 ClassLoader 的每个实例都有一个关联的父类加载器。当请求查找类或资源时,ClassLoader 实例会在尝试查找类或资源本身之前,将对类或资源的搜索委托给其父类加载器。
虚拟机的内置类加载器(称为“引导类加载器”)本身不具有父级,但可以作为 ClassLoader 实例的父级。
支持并发加载类的类加载器称为并行加载类加载器,需要通过调用 ClassLoader.registerAsParallelCapable
方法在类初始化时注册自己。请注意,ClassLoader 类默认注册为并行。但是,它的子类如果具有并行能力,仍然需要注册自己。
在委托模型不是严格分层的环境中,类加载器需要具有并行能力,否则类加载会导致死锁,因为加载器锁在类加载过程的持续时间内保持不变(请参阅 loadClass
方法)。
通常,Java 虚拟机以与平台相关的方式从本地文件系统加载类。例如,在 UNIX 系统上,虚拟机从 CLASSPATH
环境变量定义的目录中加载类。
但是,某些类可能不是源自文件;它们可能来自其他来源,例如网络,或者它们可以由应用程序构建。方法 defineClass
将字节数组转换为类 Class 的实例。可以使用 Class.newInstance
创建此新定义的类的实例。
由类加载器创建的对象的方法和构造函数可以引用其他类。要确定所引用的类,Java 虚拟机将调用最初创建该类的类加载器的 loadClass
方法。
例如,应用程序可以创建网络类加载器以从服务器下载类文件。示例代码可能如下所示:
ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();
. . .
网络类加载器子类必须定义方法 findClass
和 loadClassData
以从网络加载类。一旦下载了构成类的字节,就应该使用 defineClass
方法创建一个类实例。示例实现是:
class NetworkClassLoader extends ClassLoader {
String host;
int port;
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the class data from the connection
. . .
}
}
内置类加载器类型
让我们首先学习如何使用各种类加载器使用一个简单示例加载不同的类:
public void printClassLoaders() throws ClassNotFoundException {
System.out.println("Classloader of this class:"
+ PrintClassLoader.class.getClassLoader());
System.out.println("Classloader of Logging:"
+ Logging.class.getClassLoader());
System.out.println("Classloader of ArrayList:"
+ ArrayList.class.getClassLoader());
}
执行时,上面的方法打印:
Class loader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Class loader of Logging:sun.misc.Launcher$ExtClassLoader@3caeaf62
Class loader of ArrayList:null
我们可以看到,这里有三种不同的类加载器:应用程序,扩展和引导程序(显示为 null)。
应用程序类加载器加载包含示例方法的类。应用程序或系统类加载器在类路径中加载我们自己的文件。
接下来,扩展程序加载 Logging 类。扩展类加载器加载类,它们是标准核心 Java 类的扩展。
最后,bootstrap 加载 ArrayList 类。引导或原始类加载器是所有其他加载器的父级。
但是,我们可以看到最后输出,对于 ArrayList,它在输出中显示为 null。这是因为引导类加载器是用本地代码(native code)而不是 Java 编写的 - 因此它不会显示为 Java 类。由于这个原因,引导类加载器的行为将在 JVM 之间不同。
现在让我们更详细地讨论每个类加载器。
引导(Bootstrap)类加载器
Java 类由 java.lang.ClassLoader
的实例加载。但是,类加载器本身就是类。因此,问题是,谁加载 java.lang.ClassLoader
本身?
这就需要介绍到 引导或原始类加载器。它主要负责加载 JDK 内部类,通常是 rt.jar
和位于$JAVA_HOME/jre/lib
目录中的其他核心库。此外,引导类加载器充当所有其他 ClassLoader 实例的父级。
此引导类加载器是核心 JVM 的一部分,并使用本地代码(native code)编写,如上例所示。不同的平台可能具有此特定类加载器的不同实现。
扩展类加载器
扩展类加载器是引导类加载器的子类,负责加载标准核心 Java 类的扩展,以便它可供平台上运行的所有应用程序使用。
扩展类加载器从 JDK 扩展目录加载,通常是 $JAVA_HOME/lib/ext
目录或 java.ext.dirs
系统属性中提到的任何其他目录。
系统类加载器
另一方面,系统或应用程序类加载器负责将所有应用程序级别类加载到 JVM 中。它加载在类路径环境变量 - classpath 或 -cp 命令行选项中找到的文件。此外,它是 扩展类加载器的子类。
类装载机如何工作?
类加载器是 Java 运行时环境的一部分。当 JVM 请求类时,类加载器会尝试使用完全限定的类名来定位类并将类定义加载到运行时。
java.lang.ClassLoader.loadClass()
方法负责将类定义加载到运行时。它尝试基于完全限定名称加载类。
如果尚未加载该类,它会将请求委托给父类加载器。此过程以递归方式发生。
最终,如果父类加载器没有找到该类,则子类将调用 java.net.URLClassLoader.findClass()
方法来查找文件系统本身中的类。
如果最后一个子类加载器也无法加载该类,则会抛出 java.lang.NoClassDefFoundError
或 java.lang.ClassNotFoundException
。
让我们看一下抛出 ClassNotFoundException
时的输出示例。
java.lang.ClassNotFoundException: com.baeldung.classloader.SampleClassLoader
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
如果我们通过查看 java.lang.Class.forName()
来查看事件序列,我们可以理解它首先尝试通过父类加载器加载类,然后 java.net.URLClassLoader.findClass()
来查找类本身。当它仍然没有找到该类时,它会抛出一个 ClassNotFoundException
。
类加载器有三个重要特性。
双亲委派模型
类加载器遵循委托模型,在请求查找类或资源时,ClassLoader
实例将类或资源的搜索委托给父类加载器。
假设我们有一个将应用程序类加载到 JVM 的请求。系统类加载器首先将该类的加载委托给其父扩展类加载器,后者又将其委托给引导类加载器。
只有当引导类加载器和扩展类加载器在加载类时不成功时,系统类加载器才会尝试加载类本身。
唯一类
作为委托模型的结果,我们总是尝试向上委托,因此很容易确保唯一的类。如果父类加载器无法找到该类,则只有当前实例本身才会尝试这样做。
可见性
此外,子类加载器对其父类加载器加载的类是可见的。
例如,系统类加载器加载的类可以看到扩展和引导类加载器加载的类,但反之亦然。
为了说明这一点,如果类 A 由应用程序类加载器加载并且类 B 由扩展类加载器加载,则就应用程序类加载器加载的其他类而言,A 和 B 类都是可见的。
尽管如此,就扩展类加载器加载的其他类而言,B 类是唯一可见的类。
上下文类加载器
通常,上下文类加载器为 J2SE 中引入的类加载委派方案提供了另一种方法。
就像我们之前学到的一样,JVM 中的类加载器遵循层次模型,这样每个类加载器都有一个父级,除了引导类加载器。
但是,有时当 JVM 核心类需要动态加载应用程序开发人员提供的类或资源时,我们可能会遇到问题。
例如,JNDI 服务,JNDI 现在已经是 Java 的标准服务,核心功能由 rt.jar 中的引导类加载。但 JNDI 的目的就是对资源进行集中管理和查找,这些 JNDI 类可能会加载由独立供应商实现的 JNDI 接口提供者(部署在应用程序类路径中)SPI 的代码。此方案要求引导类加载器(父类加载器)加载应用程序加载器(子类加载器)可见的类,但启动类不可能“认识”这些代码,怎么办?
为了解决该问题,Java 设计团队引出了一个不太优雅的实现;线程上下文类加载器(Thread Context ClassLoader)。
java.lang.Thread
类有一个方法 getContextClassLoader()
,它返回特定线程的 ContextClassLoader
。在加载资源和类时,ContextClassLoader
由线程的创建者提供。
如果创建线程时未设置该值,则默认从父线程的类加载器上下文继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文类加载器,JNDI 服务就可以使用这个线程上下文加载器去加载所需要的 SPI 代码,也就是父类加载器请求子类加载器去完成类加载器的动作。
Java 中所有设计 SPI 的加载动作基本上都采用这种方式,例如 JNDI,JDBC,JAXB 和 JBI 等。
非标准的加载模型
Sun 公司所提出的模块化规范在与 JCP 组织的模块化之争中落败给 JSR-291(OSGI R4.2),虽然 Sum 不赶失去 Java 模块化的主导权,独立在发展 Jigsaw 项目,但目前 OSGI 已经成为业界“事实上”的 Java 模块化标准,而 OSGI 实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(OSGI 中称为 Bundle)都有一个自己的类加载器,当需要更换一个 Bundle时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。
OSGi 中,类加载器不再是双亲委派模型下的树状结构,而是进一步发展为更加复杂的网状结构。
二进制名称(完全限定名称)
作为 ClassLoader 中的方法的 String 参数提供的任何类名必须是由 Java™ 语言规范定义的二进制名称。 有效类名的示例包括:
"java.lang.String"
"javax.swing.JSpinner$DefaultEditor"
"java.security.KeyStore$Builder$FileBuilder$1"
"java.net.URLClassLoader$3$1"