通过上一篇Java的类加载机制相信大家已经搞明白了整个类加载从触发时机,接着我们就来看下类加载器,因为类加载机制是有加载器实现的。
类加载器的分类
启动类加载器
Bootstrap ClassLoader 是 Java 虚拟机(JVM)的一部分,它负责加载 Java 核心库,也就是 Java Runtime Environment (JRE) 中的类。这些类通常位于 JRE 的 lib
目录下的 rt.jar
文件中,以及可能的其他 JAR 文件,比如 jsse.jar
(Java Secure Socket Extension)等。
Bootstrap ClassLoader 是一个虚拟的类加载器,它不继承自 java.lang.ClassLoader
,也不可以被直接引用或实例化。它是 JVM 的一部分,并且是所有类加载器的父加载器。当 JVM 启动时,Bootstrap ClassLoader 首先加载 rt.jar
中的类,然后这些类可以被其他类加载器使用。
Bootstrap ClassLoader 的主要作用包括:
- 加载 Java 核心库,如
java.lang
包中的类。 - 作为类加载器层次结构的根,为其他类加载器提供基础。
扩展类加载器
Extension ClassLoader 是 Java 虚拟机(JVM)中的一个系统类加载器,它负责加载 Java 扩展目录中的类库。以下是关于 Extension ClassLoader 的几个关键点:
-
加载职责:Extension ClassLoader 专门用来加载
lib/ext
目录或者由系统属性java.ext.dirs
指定的其他目录中的类库。这些类库通常包括 Java 平台的标准扩展,例如一些常用的第三方库。 -
类加载顺序:在 JVM 的类加载体系中,Extension ClassLoader 位于 Bootstrap ClassLoader 之后。当一个类请求被提交到 JVM 时,Bootstrap ClassLoader 首先尝试加载,如果找不到相应的类,请求会传递给 Extension ClassLoader。
-
双亲委派模型:Extension ClassLoader 遵循 Java 的双亲委派模型,这意味着在尝试自己加载类之前,它会先委托给父类加载器(在这个情况下是 Bootstrap ClassLoader)进行加载。
-
配置灵活性:开发者可以通过设置
java.ext.dirs
系统属性来指定多个扩展目录,使得 JVM 可以在启动时从这些目录中加载类库。
应用类加载器
Application ClassLoader 是 JVM 中的一个系统类加载器,它的作用是加载应用程序类路径(ClassPath)上指定的类库。以下是 Application ClassLoader 的几个关键点:
1. 加载职责:Application ClassLoader 主要负责加载用户编写的 Java 应用程序代码。这些代码通常位于项目的 `bin` 或 `classes` 目录下,或者是通过 JAR 文件提供的。
2. 类加载顺序:在 JVM 的类加载器层级中,Application ClassLoader 位于 Extension ClassLoader 之后。这意味着,如果一个类同时在扩展目录和应用程序类路径中存在,Extension ClassLoader 会优先加载它。
3. 双亲委派模型:和 Extension ClassLoader 一样,Application ClassLoader 也遵循双亲委派模型。当它需要加载一个类时,会先委托给父类加载器(Extension ClassLoader)尝试加载,如果父类加载器无法加载,Application ClassLoader 才会尝试从应用程序类路径加载。
4. 灵活性:开发者可以通过设置系统属性 `java.class.path` 或使用 `-cp` 或 `-classpath` 命令行选项来指定应用程序类路径,从而控制 Application ClassLoader 加载的类库。
简而言之,Application ClassLoader 是我们程序员在 Java 应用程序开发过程中接触最多的类加载器,它负责将编写的 Java 类加载到 JVM 中,是 Java 应用程序运行的基础。其实你大致就理解为去加载你写好的Java代码,这个类加载器就负责加载你写好的那些类到内存里
自定义类加载器
自定义类加载器是 Java 动态加载类的一种强大机制,它允许开发者根据特定的需求来加载类。这种机制特别有用在需要动态加载或更新类定义的场景中,例如在热部署、模块化应用、或者需要从非标准源加载类文件等情况下。以下是 自定义类加载器的几个关键点:
-
继承性:自定义类加载器需要继承
java.lang.ClassLoader
类,并重写findClass
方法来实现自定义的类查找逻辑。 -
加载逻辑:开发者可以在
findClass
方法中实现自己的类加载逻辑,例如从数据库、网络、文件系统等非标准位置加载类文件。 -
双亲委派模型:自定义类加载器同样遵循 Java 的双亲委派模型。在尝试加载类之前,它会委托给父类加载器,如果父类加载器无法加载,自定义类加载器才会介入。
-
隔离性:自定义类加载器可以创建与系统类加载器和应用程序类加载器隔离的类,这有助于实现模块化和防止类冲突。
-
安全性:自定义类加载器可以提供额外的安全检查,例如验证类文件的来源或内容,确保加载的类是安全的。
-
灵活性:通过自定义类加载器,开发者可以控制类的加载时机、来源和方式,为 Java 应用程序提供更高的灵活性和可扩展性。
-
使用场景:自定义类加载器适用于需要动态加载类、实现类版本控制、或者需要加载特定格式类文件的应用程序。
通过自定义类加载器,Java 应用程序可以突破传统的类加载限制,实现更加灵活和动态的类加载策略。这对于需要高度定制化和模块化的应用程序尤其重要。下面提供简单的代码实例:
import java.io.*;
import java.nio.file.*;
import java.util.logging.*;
public class MyClassLoader extends ClassLoader {
private static final Logger LOGGER = Logger.getLogger(MyClassLoader.class.getName());
private final String classPath;
private final boolean verify;
public MyClassLoader(String classPath) {
this(classPath, true);
}
public MyClassLoader(String classPath, boolean verify) {
this.classPath = classPath;
this.verify = verify;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = name.replace('.', '/') + ".class";
byte[] classData = loadClassData(fileName);
if (classData == null) {
throw new ClassNotFoundException("Could not find " + fileName + " in " + classPath);
}
if (verify) {
verifyClassData(classData);
}
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String fileName) throws IOException {
Path path = Paths.get(classPath, fileName);
try (InputStream in = Files.newInputStream(path)) {
return in.readAllBytes();
}
}
private void verifyClassData(byte[] classData) {
// 这里可以添加对类数据的验证逻辑
LOGGER.info("Class data verification is not implemented yet.");
}
public static void main(String[] args) {
try {
String classPath = "path/to/your/classes"; // 替换为实际的类文件路径
MyClassLoader myClassLoader = new MyClassLoader(classPath);
Class<?> myClass = myClassLoader.loadClass("com.example.MyClass");
Object instance = myClass.getDeclaredConstructor().newInstance();
// 假设 MyClass 有一个 sayHello 方法
java.lang.reflect.Method sayHelloMethod = myClass.getMethod("sayHello");
sayHelloMethod.invoke(instance);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error loading class", e);
}
}
}
双亲委派机制
双亲委派机制层级结构
JVM的类加载器是有亲子层级结构的,就是说启动类加载器是最上层的,扩展类加载器在第二层,第三层是应用程序类加载器,最后一层是自定义类加载器,虽然说是最后一层,但是因为我们可以无限自定义,所以其实就是树的结构,深度可以不断累加。如下图:
为什么需要双亲委派机制?
双亲委派机制是 Java 类加载机制的核心原则之一,其设计具有以下几个重要目的:
-
安全性:双亲委派机制确保了 Java 核心库的类不能被随意替换或篡改。因为只有引导类加载器(Bootstrap ClassLoader)才能加载
rt.jar
中的 Java 核心类库,而它是不可扩展的。这防止了恶意代码对 Java 核心库的破坏。 -
避免类的重复加载:通过委托给父类加载器,JVM 确保了一个类在 JVM 内只被加载一次。这避免了类的重复加载,节省了内存资源,并且保证了类的唯一性。
-
保证加载顺序:双亲委派模型保证了类按照既定的顺序加载,例如,Java 核心库总是首先加载,然后是扩展库,最后是应用程序类。这有助于维护程序的稳定性和可预测性。
-
封装性和层次性:双亲委派模型允许 Java 应用程序分层,每一层都有自己的类加载器。这有助于实现模块化,使得不同层次的代码可以独立地更新和替换,而不会影响到其他层次。
-
类空间隔离:在复杂的应用程序中,如 Web 容器或 OSGi 框架,双亲委派模型通过使用不同的类加载器来实现类空间的隔离。这使得不同的模块或应用可以加载相同类的不同版本,而不会相互冲突。
-
易于实现和维护:双亲委派模型的实现相对简单,逻辑清晰,易于理解和维护。开发者可以专注于实现自己的
findClass
方法,而不必担心类加载的委托逻辑。 -
灵活性:虽然双亲委派模型是 Java 类加载的默认策略,但它并不是强制性的。开发者可以通过自定义类加载器来实现特定的类加载逻辑,例如,从数据库或网络加载类。
-
优化性能:通过缓存已加载的类(在
ClassLoader
中的classes
集合中),JVM 可以快速检查类是否已经被加载,从而避免不必要的加载过程,提高性能。
简而言之,双亲委派机制为 Java 类加载提供了一种安全、有效、可预测的策略,有助于维护 Java 应用程序的稳定性和性能。
Tomcat真的破双亲委派机制了嘛?
Tomcat 并没有打破 Java 的双亲委派模型,而是在双亲委派模型的基础上进行了适应 Web 容器需求的扩展。下面是几个关键点来说明这一点:
-
双亲委派模型的核心:双亲委派模型的核心是,当一个类加载器收到类加载请求时,它会先委托给父类加载器去尝试加载这个类,直到达到启动类加载器。如果父类加载器无法完成加载任务,才会由子类加载器尝试加载。
-
Tomcat 类加载器的实现:Tomcat 实现了多个层次的类加载器,包括 Common ClassLoader、Server ClassLoader、Shared ClassLoader 和 Webapp ClassLoader。这些类加载器都遵循双亲委派模型的委派逻辑。
-
Webapp ClassLoader 的特殊性:虽然 Tomcat 的 Webapp ClassLoader 允许 Web 应用加载自己版本的类,但这是通过实现自己的
findClass
方法来实现的,而不是通过重写loadClass
方法来绕过双亲委派模型。Webapp ClassLoader 仍然会首先委托父类加载器尝试加载类。 -
线程上下文类加载器(TCCL):Tomcat 在执行请求时,会将当前 Web 应用的类加载器设置为 TCCL。这确保了请求处理过程中加载的类首先会使用 Webapp ClassLoader,但这个过程仍然是在双亲委派模型的框架内进行的。
-
隔离性和灵活性:Tomcat 的类加载器设计提供了类隔离和版本控制的灵活性,这并不违反双亲委派模型,而是扩展了类加载器的使用场景。
类加载器实际的关系
双亲委派机制,最早我一直以为是子类继承父类,其实看到源码以后发现实际上并没有继承关系,而是有成员变量取名叫parent,所以‘亲’由此而来;他们之间是组合关系并非是继承嗷!
说到这里,本来该结束了,突然想到Effective Java中有一条原则是:组合优于继承?有小伙伴知道吗?哈哈这里就不展开了,后续会在设计模式模块好好唠唠。
结合上述类加载器,回头看看类加载机制忘了的小伙伴可以看上篇文章Java的类加载机制,整体流程如下图:
现在我们已经了解到了如何把文件加载到JVM里了,那从JVM角度考虑这些类的数据应该存放到什么位置呢?运行时候产生的动态数据又该放到哪里呢?
专题汇总
JVM专题一:深入分析Java工作机制
JVM专题二:Java如何进行编译的
JVM专题三:Java代码如何运行
JVM专题四:JVM的类加载机制
JVM专题五:类加载器与双亲委派机制
JVM专题六:JVM的内存模型
JVM专题七:JVM垃圾回收机制
JVM专题八:JVM如何判断可回收对象
JVM专题九:JVM分代知识点梳理
JVM专题十:JVM中的垃圾回收机制
JVM专题十一:JVM 中的收集器一
JVM专题十二:JVM 中的收集器二
JVM专题十三:总结与整理(面试常用)