Java 程序代码经过编译后会产生 Class 文件,而这些 Class 文件最终需要加载到虚拟机中才能够被运行和使用。而虚拟机是如何加载这些文件的,Class 文件在进入到虚拟机后又会产生什么变化,这些都是在本篇讲述的内容。
Java 虚拟机把描述类的数据从 Class 文件加载到内存中,并对数据进行验证、转换解析和初始化,最终形成虚拟机可以直接使用的 Java 类型的过程就是类加载机制。
类加载过程如下图:
类加载器的作用是根据一个类的全限定名查找 .class 文件,读取它的二进制流,并把创建 java.lang.Class 文件加载到虚拟机的内存中。
类的唯一性是由类的本身和它的类加载器决定的,也就是说两个类来源于同一个 Class 文件,并且被同一个类加载器加载,这两个类才相等。
虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)。
<JAVA_HOME>/lib
路径下的核心类库或 -Xbootclasspath
参数指定的路径下的 jar 包加载到内存中,由于虚拟机是按照文件名识别并加载 jar 包的,如果文件名不被虚拟机识别,即使把 jar 包丢到 lib 目录下也是没有作用的,出于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类。sun.misc.Launcher$ExtClassLoader
类,由 Java 语言实现的,是 Launcher 的静态内部类,它负责加载 <JAVA_HOME>/lib/ext
目录下或者由系统变量 -Djava.ext.dir
指定位路径中的类库,开发者可以直接使用标准扩展类加载器。sun.misc.Launcher$AppClassLoader
。它负责加载系统类路径java -classpath
或 -D java.class.path
指定路径下的类库,也就是我们经常用到的 classpath 路径,开发者可以直接使用系统类加载器,一般情况下该类加载器是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()
方法可以获取到该类加载器。除此之外,开发者还可以自己定义类加载器。Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的 class 文件加载到内存生成 Class 对象,而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式即把请求交由父类处理。
系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的 loadClass()
处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader
中。当父类加载器无法处理时,才调用自身的 findClass()
方法加载。当父类加载器为 null 时,会使用启动类加载器 BootstrapClassLoader
作为父类加载器。
简单来说,双亲委派模型通过一种层级关系来保证类的安全性和避免类冲突。根据这个模型,当一个类需要被加载时,Java 虚拟机会按照一定的顺序依次向上委派给父类加载器进行加载,直到最顶层的启动类加载器,如果顶层的启动类加载器无法加载该类,再逐级向下委派给子类加载器进行加载。
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
c = parent.loadClass(name, false);
} else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明父类加载器无法完成加载请求
}
if (c == null) {
long t1 = System.nanoTime();
//自己尝试加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载( JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object
类。
然而,这个双亲委派模型并不是绝对安全的,有时候它可能会被破坏。以下是一些可能破坏双亲委派模型的情况:
Java 允许开发者自定义类加载器,这些自定义类加载器可以通过继承java.lang.ClassLoader
类来实现。如果开发者在自定义类加载器中重写了类加载的逻辑,可能会绕过双亲委派模型,直接加载指定的类,从而破坏了原本的类加载层次结构。
public class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.startsWith("com.aysaml")) {
// 在自定义类加载器中直接加载 com.aysaml 包下的类
return findClass(name);
}
return super.loadClass(name);
}
}
在上面代码中,自定义的CustomClassLoader
继承了ClassLoader
类,并重写了loadClass
方法。当加载的类名以"com.aysaml"开头时,它会绕过双亲委派模型,直接在自定义类加载器中加载该类。
Java 提供了线程上下文类加载器(Thread Context ClassLoader)的机制,允许线程在运行时动态改变类的加载行为。如果代码中使用了线程上下文类加载器,并且该加载器没有遵循双亲委派模型,就可能破坏了原本的加载顺序。
public class CustomTask implements Runnable {
@Override
public void run() {
// 获取当前线程的上下文类加载器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
// 使用线程上下文类加载器加载指定类
Class<?> clazz = classLoader.loadClass("com.aysaml.MyClass");
// 执行相应的操作
// ...
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
上面代码中,CustomTask
类实现了Runnable
接口,并在run()
方法中获取了当前线程的上下文类加载器。然后使用线程上下文类加载器加载了"com.aysaml.MyClass"类,绕过了双亲委派模型。
SPI 是 Java 提供的一种服务扩展机制,允许开发者在运行时加载并实例化某个接口的实现类。SPI机制中使用了java.util.ServiceLoader
类来加载实现类,而该类使用了线程上下文类加载器,因此在某些情况下可能会绕过双亲委派模型。
假设我们有一个接口DatabaseDriver
和一个实现类MySqlDriver
,我们将MySqlDriver
的实现类名称放在META-INF/services
目录下的com.aysaml.DatabaseDriver
文件中。在这种情况下,ServiceLoader
类将会使用线程上下文类加载器来加载实现类。
ServiceLoader<DatabaseDriver> serviceLoader = ServiceLoader.load(DatabaseDriver.class);
Iterator<DatabaseDriver> drivers = serviceLoader.iterator();
while (drivers.hasNext()) {
DatabaseDriver driver = drivers.next();
// 执行相应的操作
// ...
}
上面代码中,ServiceLoader
使用了线程上下文类加载器来加载DatabaseDriver
的实现类,绕过了双亲委派模型。
这些情况下,如果不小心使用或者实现了自定义类加载器、线程上下文类加载器或 SPI 机制,可能会破坏双亲委派模型,导致类加载时的行为与原本预期不符。这可能会引发类冲突、安全性问题或者其他意料之外的行为。因此,在 Java 开发中,应该谨慎使用和实现这些机制,以避免破坏双亲委派模型所带来的问题。
模块化的类加载器是 Java 9 及其以后版本引入的概念,它与传统的类加载器有所不同。在模块化系统中,应用程序被组织成一系列模块,每个模块都有自己的类路径和依赖关系。模块化类加载器负责加载和管理这些模块。
模块化命名空间:每个模块都有一个唯一的模块名,模块名用于标识模块并区分其他模块。模块之间的类是相互隔离的,不同模块中的相同类名不会发生冲突。
显式依赖关系:模块之间通过显式声明依赖关系来进行引用。模块可以指定它所依赖的其他模块,这样在加载模块时会自动解析和加载其依赖的模块。
模块路径:与传统的类路径不同,模块路径是指定模块的位置和依赖关系的机制。它由一组模块化的 JAR 文件或目录组成,而不是单个的类路径。
模块化类加载器:模块化类加载器由启动类加载器和应用程序类加载器构成。启动类加载器负责加载 Java 运行时的核心模块,应用程序类加载器负责加载应用程序的模块。每个模块都由自己的类加载器进行加载和管理。
通过模块化类加载器,Java 应用程序可以更好地管理和隔离模块之间的依赖关系,避免了类冲突和版本冲突的问题。同时,模块化的类加载器提供了更细粒度的控制,允许开发者按需加载和卸载模块,提高了应用程序的灵活性和可维护性。