类加载机制深度解析
面试官:说说 Java 的类加载机制吧,什么是双亲委派模型?
你:双亲委派模型是 Java 类加载器的协作机制。当类加载器收到加载请求时,先委托父加载器,只有父加载器无法加载时才自己加载。这保证了核心类的安全性和唯一性。
面试官:那 Tomcat 为什么要破坏双亲委派模型?
这个问题考察的是对框架类加载机制的深入理解。能说清 Tomcat 的 WebAppClassLoader 和线程上下文类加载器的候选人,才是真正的 JVM 高手。
链式追问一:类加载的五个阶段
Section titled “链式追问一:类加载的五个阶段”Q1:类加载包含哪五个阶段?各自做了什么?必考
Section titled “Q1:类加载包含哪五个阶段?各自做了什么?”类加载流程:
一个 .class 文件从磁盘到可以执行,要经过五个阶段:
加载(Loading) → 验证(Verification) → 准备(Preparation) ↓ 解析(Resolution) ← 初始化(Initialization)各阶段详解:
1. 加载(Loading)
动作: 1. 通过类的全限定名获取 .class 文件的二进制字节流 (来源:磁盘、网络、数据库、动态生成等) 2. 将字节流代表的静态存储结构转化为方法区的运行时数据结构 3. 在堆中生成代表这个类的 java.lang.Class 对象(作为方法区数据的入口)2. 验证(Verification)
确保 .class 文件的字节流符合 JVM 规范,不会危害 JVM 安全:
验证内容:├── 文件格式验证(魔数 0xCAFEBABE、版本号等)├── 元数据验证(语义合法性,如类是否继承了 final 类)├── 字节码验证(指令逻辑是否合法)└── 符号引用验证(引用的类、方法、字段是否存在且有访问权限)3. 准备(Preparation)
为类变量(static 字段)分配内存,并初始化为零值:
static int count = 100; → 准备阶段 count = 0(不是100!)static final int MAX = 100; → 准备阶段 MAX = 100(常量直接赋值)
注意:实例变量在对象创建时分配,不在此阶段处理4. 解析(Resolution)
将常量池中的符号引用替换为直接引用:
符号引用:用字符串描述目标(如 com.example.MyClass)直接引用:指向内存的指针或偏移量
解析内容:├── 类或接口解析├── 字段解析├── 方法解析└── 接口方法解析5. 初始化(Initialization)
执行类构造器 <clinit>() 方法——编译器自动收集所有类变量赋值动作和静态代码块,合并生成 <clinit>():
class Foo { static int a = 1; // 收集到 <clinit> static { a = 2; // 收集到 <clinit> System.out.println("init"); // 收集到 <clinit> } static int b = a + 1; // 收集到 <clinit>,执行时 a=2,所以 b=3}// <clinit> 执行顺序按源码顺序:// a=1 → a=2 → b=3对比表格:
| 阶段 | 作用 | 关键操作 | 是否执行代码 |
|---|---|---|---|
| 加载 | 加载 .class 文件 | 读取字节流,创建 Class 对象 | 否 |
| 验证 | 确保安全性 | 格式、语义、字节码验证 | 否 |
| 准备 | 分配内存 | static 字段初始化为零值 | 否 |
| 解析 | 解析符号引用 | 符号引用 → 直接引用 | 否 |
| 初始化 | 执行初始化代码 | 执行 <clinit>() | 是 |
本质一句话:加载读文件,验证保安全,准备分内存,解析换引用,初始化执行代码。
Q2:什么时候会触发类的初始化?什么是主动使用和被动使用?高频
Section titled “Q2:什么时候会触发类的初始化?什么是主动使用和被动使用?”主动使用(触发初始化):
只有以下 6 种情况会触发类的初始化,称为主动使用:
1. new 一个类的实例 └── new MyClass()
2. 调用类的静态方法 └── MyClass.staticMethod()
3. 读取或设置类的静态字段(非常量) └── MyClass.staticField = 100 └── int x = MyClass.staticField └── 注意:读取 static final 常量不触发(编译期已内联)
4. 使用反射 └── Class.forName("com.example.MyClass")
5. 初始化子类时,若父类未初始化,先初始化父类 └── class Child extends Parent └── new Child() → 先初始化 Parent,再初始化 Child
6. JVM 启动时的主类(含 main() 的类) └── java MyApp → 初始化 MyApp 类被动使用(不触发初始化):
class Parent { static { System.out.println("Parent init"); } static int VALUE = 100;}
class Child extends Parent { static { System.out.println("Child init"); }}
// 场景1:通过子类引用父类的静态字段System.out.println(Child.VALUE); // 只输出 "Parent init"// 原因:访问的是父类的静态字段,不触发子类初始化
// 场景2:定义数组引用Child[] arr = new Child[10]; // 不触发 Child 初始化// 原因:数组类型由 JVM 自动生成,不触发元素类型初始化
// 场景3:访问编译期常量class Constants { static final int MAX = 100; // 编译期常量 static { System.out.println("Constants init"); }}System.out.println(Constants.MAX); // 不输出任何内容// 原因:编译期常量在编译时已内联到调用处,不需要加载类对比表格:
| 操作 | 是否主动使用 | 是否触发初始化 | 原因 |
|---|---|---|---|
new MyClass() | 是 | 是 | 创建实例 |
MyClass.staticMethod() | 是 | 是 | 调用静态方法 |
MyClass.staticField = 100 | 是 | 是 | 设置静态字段 |
int x = MyClass.MAX(final) | 否 | 否 | 编译期常量已内联 |
Child.VALUE(父类字段) | 否(对 Child) | 否(对 Child) | 访问的是父类字段 |
Child[] arr = new Child[10] | 否 | 否 | 数组类型是 JVM 生成的 |
Class.forName("MyClass") | 是 | 是 | 反射调用 |
本质一句话:只有主动使用才触发初始化,被动使用(数组、父类字段、编译期常量)不触发。
Q3:<clinit>() 方法有什么特点?如何保证线程安全?中频
Section titled “Q3:<clinit>() 方法有什么特点?如何保证线程安全?”<clinit>() 的特点:
- 编译器自动生成:收集所有类变量赋值和静态代码块,按源码顺序合并
- 父类优先:子类
<clinit>()执行前,先执行父类的<clinit>() - 可选:如果没有静态变量和静态代码块,则不生成
<clinit>() - 线程安全:JVM 保证
<clinit>()在多线程环境下被正确加锁同步,只执行一次
线程安全机制:
public class Singleton { // 饿汉式单例,利用类初始化的线程安全 private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() { return INSTANCE; }}
// 线程安全原理:// 1. JVM 保证 <clinit>() 在多线程环境下被正确加锁// 2. 多个线程同时初始化 Singleton 类时,只有一个线程执行 <clinit>()// 3. 其他线程阻塞等待,直到 <clinit>() 执行完成// 4. INSTANCE 只被初始化一次,线程安全对比:懒汉式需要显式同步:
public class LazySingleton { private static volatile LazySingleton instance;
private LazySingleton() {}
// 需要双重检查锁定 public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; }}本质一句话:<clinit>() 由 JVM 保证线程安全,是饿汉式单例的底层原理。
链式追问二:双亲委派模型
Section titled “链式追问二:双亲委派模型”Q1:什么是双亲委派模型?它的好处是什么?必考
Section titled “Q1:什么是双亲委派模型?它的好处是什么?”类加载器的层次结构:
Bootstrap ClassLoader(启动类加载器)├── 加载范围:$JAVA_HOME/lib(rt.jar、core.jar 等)├── 由 C++ 实现,在 Java 中表示为 null└── 加载 java.lang.*、java.util.* 等核心类
Extension ClassLoader / Platform ClassLoader(扩展/平台类加载器)├── 加载范围:$JAVA_HOME/lib/ext(Java 9+ 改为 Platform)└── 加载一些扩展库
Application ClassLoader(应用类加载器,也叫系统类加载器)├── 加载范围:classpath 下的类(用户写的代码、第三方 jar)└── ClassLoader.getSystemClassLoader() 获取的就是这个
自定义类加载器(Custom ClassLoader)└── 用户继承 ClassLoader 实现的,可加载网络、数据库中的类双亲委派的工作流程:
当 Application ClassLoader 要加载 com.example.MyClass:
Application ClassLoader ↓ 委托(不直接加载,先问父亲)Extension ClassLoader ↓ 委托Bootstrap ClassLoader ↓ 尝试加载 如果能加载(核心库)→ 返回 Class,向下传递 如果不能加载(核心库里没有)→ 返回失败,让子加载器尝试 ↑ 失败Extension ClassLoader 尝试加载扩展库 → 找不到 → 失败 ↑ 失败Application ClassLoader 在 classpath 中加载 → 成功 → 返回 Class代码示例:
// ClassLoader 的 loadClass 方法简化版protected Class<?> loadClass(String name, boolean resolve) { // 1. 检查是否已加载 Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { // 2. 委托父加载器 c = parent.loadClass(name, false); } else { // 3. 委托 Bootstrap ClassLoader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 父加载器无法加载,捕获异常 }
if (c == null) { // 4. 父加载器无法加载,自己尝试 c = findClass(name); } } return c;}三大好处:
| 好处 | 说明 | 示例 |
|---|---|---|
| 安全性 | 核心类库不会被用户类覆盖 | 用户写的 java.lang.String 不会被加载,始终是 Bootstrap 加载的官方 String |
| 唯一性 | 同一个类只被一个加载器加载一次 | 避免重复加载浪费方法区空间 |
| 有序性 | 越基础的类由越上层的加载器加载 | 核心类先于应用类加载,保证稳定 |
本质一句话:双亲委派是”先问父亲,父亲不行自己来”,保证核心类的安全性和唯一性。
Q2:如何判断两个类是否相等?为什么要有不同的类加载器?高频
Section titled “Q2:如何判断两个类是否相等?为什么要有不同的类加载器?”类相等的条件:
两个类相等,必须满足两个条件:
- 全限定名相同(包名 + 类名)
- 类加载器相同(必须是同一个 ClassLoader 实例)
// 示例:不同类加载器加载的同名类不相等ClassLoader loader1 = new MyClassLoader();ClassLoader loader2 = new MyClassLoader();
Class<?> c1 = loader1.loadClass("com.example.MyClass");Class<?> c2 = loader2.loadClass("com.example.MyClass");
System.out.println(c1 == c2); // false!// 虽然类名相同,但加载器不同,JVM 视为不同的类
// 实际应用:// Tomcat 的不同 WebApp 有不同的类加载器// WebApp A 和 WebApp B 可以加载不同版本的同名类,互不干扰为什么要有不同的类加载器:
1. 安全性 ├── Bootstrap ClassLoader 加载核心类,防止篡改 └── 用户无法自定义类加载器加载核心类
2. 隔离性 ├── 不同类加载器加载的同名类不相等 └── Tomcat、OSGi 等框架用自定义类加载器隔离不同应用
3. 灵活性 ├── 自定义类加载器可从网络、数据库加载类 └── 支持热部署、动态更新
4. 扩展性 ├── Java 9 模块化系统用多个类加载器 └── 每个模块可以有自己的类加载器本质一句话:类加载器是类的”命名空间”,不同加载器加载的同名类不相等,实现隔离。
Q3:如何实现自定义类加载器?需要注意什么?中频
Section titled “Q3:如何实现自定义类加载器?需要注意什么?”自定义类加载器步骤:
- 继承
ClassLoader类 - 重写
findClass()方法(不要重写loadClass(),破坏双亲委派) - 在
findClass()中读取类字节码,调用defineClass()生成 Class 对象
代码示例:
public class DiskClassLoader extends ClassLoader { private String classPath;
public DiskClassLoader(String classPath) { this.classPath = classPath; }
@Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { // 1. 读取 .class 文件 byte[] data = loadClassData(name);
// 2. 将字节码转换为 Class 对象 return defineClass(name, data, 0, data.length); } catch (IOException e) { throw new ClassNotFoundException(name, e); } }
private byte[] loadClassData(String name) throws IOException { // 将包名转换为路径 String path = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
try (InputStream is = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = is.read(buffer)) != -1) { baos.write(buffer, 0, bytesRead); } return baos.toByteArray(); } }}
// 使用:ClassLoader loader = new DiskClassLoader("/tmp/classes");Class<?> clazz = loader.loadClass("com.example.MyClass");Object obj = clazz.newInstance();注意事项:
1. 不要重写 loadClass(),除非要破坏双亲委派 └── loadClass() 实现双亲委派,重写会破坏 └── 只需重写 findClass(),让父类 loadClass() 调用
2. 调用 defineClass() 前确保字节码合法 └── defineClass() 会验证字节码,不合法会抛 ClassFormatError
3. 父加载器的选择 ├── 不指定 parent → 默认是 Application ClassLoader └── 显式指定 parent → new DiskClassLoader(parent)
4. 缓存已加载的类 └── ClassLoader 内部已缓存,无需手动缓存本质一句话:自定义类加载器只需重写 findClass(),不要重写 loadClass() 以保持双亲委派。
链式追问三:双亲委派的破坏
Section titled “链式追问三:双亲委派的破坏”Q1:JDBC 的驱动加载是如何打破双亲委派的?为什么要打破?必考
Section titled “Q1:JDBC 的驱动加载是如何打破双亲委派的?为什么要打破?”为什么要打破:
问题: java.sql.DriverManager 在 rt.jar 中,由 Bootstrap ClassLoader 加载 但 DriverManager 需要加载 com.mysql.jdbc.Driver 等第三方驱动 这些驱动在用户的 classpath 中,只有 Application ClassLoader 能加载
矛盾: Bootstrap ClassLoader(父加载器)无法委托 Application ClassLoader(子加载器) → 双亲委派是单向的,父无法向下委托子解决方案:线程上下文类加载器(Thread Context ClassLoader):
// DriverManager 的实现(JDK 源码简化)public class DriverManager { static { // 1. 获取线程上下文类加载器(通常是 Application ClassLoader) ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 2. 用上下文类加载器加载驱动 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class, cl); Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 3. 遍历加载的驱动,注册到 DriverManager while (driversIterator.hasNext()) { driversIterator.next(); } }}
// 线程上下文类加载器的设置(通常由应用启动时设置)Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());工作流程:
1. 启动应用时,主线程设置上下文类加载器为 Application ClassLoader
2. DriverManager 由 Bootstrap ClassLoader 加载,执行 static 代码块
3. DriverManager 通过 Thread.currentThread().getContextClassLoader() 获取到 Application ClassLoader
4. 用 Application ClassLoader 加载 com.mysql.jdbc.Driver
5. 本质:父加载器通过"旁门"(线程上下文)使用子加载器的能力本质一句话:线程上下文类加载器让父加载器”借用”子加载器的能力,解决 SPI 加载问题,是双亲委派的设计妥协。
Q2:Tomcat 为什么需要破坏双亲委派?它是如何隔离不同 WebApp 的?高频
Section titled “Q2:Tomcat 为什么需要破坏双亲委派?它是如何隔离不同 WebApp 的?”Tomcat 的需求:
同一个 Tomcat 实例可能部署多个 Web 应用,不同应用可能依赖同一个库的不同版本:
WebApp A:依赖 Spring 5.0WebApp B:依赖 Spring 4.0
如果用双亲委派: Application ClassLoader 先委托 Bootstrap → Extension → Application → Spring 类由 Application ClassLoader 加载一次 → WebApp A 和 B 共享同一个 Spring 类 → 版本冲突!Tomcat 的类加载体系:
Bootstrap ClassLoader └── Extension ClassLoader └── Application ClassLoader(加载 Tomcat 自身代码) └── Common ClassLoader(Tomcat + 所有 WebApp 共享库) ├── Catalina ClassLoader(Tomcat 内部私有类) └── WebApp ClassLoader × N(每个 Web 应用一个) └── JSP ClassLoader × M(每个 JSP 一个,支持热替换)WebAppClassLoader 的逻辑(违反双亲委派):
// Tomcat WebAppClassLoader 的加载顺序(简化)public Class<?> loadClass(String name) { // 1. 先查本地缓存(已加载过?) Class<?> clazz = findLoadedClass(name); if (clazz != null) { return clazz; }
// 2. 核心类(java.lang.*)必须用 Bootstrap ClassLoader if (name.startsWith("java.")) { return parent.loadClass(name); }
// 3. 先尝试从 /WEB-INF/classes 和 /WEB-INF/lib 加载(自己先来) // ← 打破双亲委派! try { clazz = findClass(name); return clazz; } catch (ClassNotFoundException e) { // 自己找不到,继续 }
// 4. 再委托父加载器(Common ClassLoader) return parent.loadClass(name);}隔离效果:
WebApp A 的 WebAppClassLoader: → 加载 /webapps/A/WEB-INF/lib/spring-5.0.jar → Spring 类在 WebApp A 的命名空间
WebApp B 的 WebAppClassLoader: → 加载 /webapps/B/WEB-INF/lib/spring-4.0.jar → Spring 类在 WebApp B 的命名空间
虽然类名相同,但加载器不同,JVM 视为不同的类→ 完全隔离,互不干扰对比表格:
| 类加载器 | 加载顺序 | 是否破坏双亲委派 | 目的 |
|---|---|---|---|
| 标准双亲委派 | 父 → 子 | 否 | 安全、唯一 |
| WebAppClassLoader | 自己 → 父 | 是 | 应用隔离 |
| JDBC SPI | 父借用子(线程上下文) | 是 | SPI 加载 |
本质一句话:Tomcat 用 WebAppClassLoader 优先加载自己应用的类,实现多应用隔离,避免版本冲突。
Q3:热部署是如何实现的?为什么热部署后内存会增加?实战
Section titled “Q3:热部署是如何实现的?为什么热部署后内存会增加?”热部署原理:
类加载器加载的类信息存储在方法区(元空间),类加载器对象本身也被 GC Root 引用(通常是线程)。要”更新”一个类,唯一的方式是:废弃旧的类加载器,用新的类加载器重新加载新版本的类。
Tomcat JSP 热替换流程:
1. 检测到 JSP 文件变化(文件最后修改时间变化)
2. 丢弃当前 JSP 对应的 JspClassLoader(断开引用)
3. 创建新的 JspClassLoader,加载新编译的 JSP 类
4. 后续请求使用新的 JSP 类内存问题(类加载器泄漏):
旧 ClassLoader 被丢弃后,它加载的所有类的元数据需要等 GC 回收
但如果旧 ClassLoader 还有活跃引用: ├── 静态缓存持有对插件类的引用 ├── 线程池中的 Runnable/Callable 持有对插件类的引用 └── 监听器未注销
→ ClassLoader 无法被 GC 回收→ 它加载的所有类的元数据无法释放→ 每次热部署泄漏一个 ClassLoader → 元空间持续增长→ 最终 OutOfMemoryError: Metaspace排查方式:
# 1. 查看类加载数量jstat -class <pid> 1000
# 2. 查看类加载器jmap -clstats <pid>
# 3. 用 VisualVM 查看类加载数量是否持续增长
# 4. 用 MAT 分析 ClassLoader 引用链# 找到谁在持有旧的 ClassLoader防止泄漏的最佳实践:
1. 避免静态字段持有对插件类的引用
2. 热卸载前清理线程池(停止所有线程)
3. 注销所有监听器和回调
4. 使用 WeakReference 持有对插件类的引用
5. 定期重启应用(最简单粗暴)本质一句话:热部署通过废弃旧类加载器实现,但类加载器泄漏会导致元空间增长,需清理所有引用。
链式追问四:实战场景
Section titled “链式追问四:实战场景”Q1:如果让你实现一个支持多版本隔离的插件系统,类加载器应该怎么设计?实战
Section titled “Q1:如果让你实现一个支持多版本隔离的插件系统,类加载器应该怎么设计?”核心设计:
1. 每个插件独立 ClassLoader └── 隔离不同插件的类
2. 宿主框架的接口类必须用父加载器 └── 保证插件和宿主用同一个 Class 对象,类型兼容
3. 插件内部的类先自己加载 └── 插件可使用不同版本的依赖库
4. 热卸载时清理所有对插件 ClassLoader 的引用 └── 避免类加载器泄漏代码示例:
public class PluginClassLoader extends URLClassLoader { private final ClassLoader parent;
public PluginClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); this.parent = parent; }
@Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. 宿主框架的接口类,必须用父加载器 // 保证插件和宿主用同一个 Class 对象 if (isHostApiClass(name)) { return parent.loadClass(name); }
// 2. 插件内部的类,先尝试自己加载(隔离) try { return findClass(name); // 从插件 jar 包加载 } catch (ClassNotFoundException e) { // 3. 找不到再委托父加载器 return parent.loadClass(name); } }
private boolean isHostApiClass(String name) { // 判断是否是宿主框架的接口类 return name.startsWith("com.host.api."); }}插件管理器:
public class PluginManager { private Map<String, PluginClassLoader> plugins = new ConcurrentHashMap<>();
// 加载插件 public void loadPlugin(String pluginId, URL[] urls) { PluginClassLoader loader = new PluginClassLoader( urls, getClass().getClassLoader() // 宿主的类加载器作为 parent ); plugins.put(pluginId, loader);
// 加载插件入口类 Class<?> entryClass = loader.loadClass("com.plugin.Entry"); Plugin plugin = (Plugin) entryClass.newInstance(); plugin.start(); }
// 卸载插件 public void unloadPlugin(String pluginId) { PluginClassLoader loader = plugins.remove(pluginId); if (loader != null) { try { loader.close(); // Java 7+ 支持关闭 URLClassLoader } catch (IOException e) { // 处理异常 } } }}关键注意事项:
1. 插件通信通过接口(宿主定义,父加载器加载) └── 宿主通过接口与插件交互,类型兼容
2. 避免宿主代码持有对插件类的强引用 └── 尤其是 static 字段,否则 ClassLoader 无法 GC
3. 插件卸载时清理所有资源 ├── 停止插件创建的线程 ├── 注销插件注册的监听器 └── 释放插件持有的外部资源(文件、连接等)本质一句话:插件系统用独立 ClassLoader 隔离,用父加载器共享接口,热卸载需清理所有引用避免泄漏。
Q2:Java 9 的模块化系统如何改变类加载机制?加分
Section titled “Q2:Java 9 的模块化系统如何改变类加载机制?”Java 9 之前(三层层类加载):
Bootstrap ClassLoader └── Extension ClassLoader └── Application ClassLoaderJava 9 之后(模块化类加载):
Bootstrap ClassLoader ├── 加载 java.base 等核心模块 └── Platform ClassLoader(原 Extension) ├── 加载 java.sql、java.xml 等平台模块 └── Application ClassLoader ├── 加载用户模块(unnamed module) └── 每个 named module 可以有独立的 ClassLoader模块化带来的变化:
1. 类加载按模块边界,不再是简单的双亲委派 └── 模块声明依赖关系,形成网状依赖图
2. 平台类加载器改名 └── Extension ClassLoader → Platform ClassLoader
3. 模块路径(module path)替代类路径(classpath) └── 模块路径中的模块由 Application ClassLoader 加载 └── 未命名模块(unnamed module)仍按传统方式加载
4. 更强的封装性 └── 模块通过 module-info.java 声明导出的包 └── 未导出的包对外不可见,即使用反射也受限制module-info.java 示例:
module com.example.myapp { requires java.sql; // 依赖 java.sql 模块 requires transitive java.logging; // 传递依赖 exports com.example.api; // 导出 API 包 opens com.example.internal to java.base; // 反射访问}本质一句话:Java 9 模块化用模块边界替代类加载器层次,形成网状依赖,增强封装性,但核心仍是分层加载。
类加载核心知识点:
- 五个阶段:加载、验证、准备、解析、初始化
- 主动使用:6 种触发初始化的场景
<clinit>():编译器自动生成,JVM 保证线程安全- 双亲委派:先委托父加载器,保证安全性和唯一性
- 类相等:全限定名相同 + 类加载器相同
- 自定义类加载器:重写
findClass(),遵守双亲委派 - 线程上下文类加载器:解决 SPI 加载问题,双亲委派的”后门”
- Tomcat 类加载:WebAppClassLoader 优先加载自己,实现应用隔离
- 热部署:废弃旧类加载器,用新类加载器重新加载
- 类加载器泄漏:旧 ClassLoader 被持有无法 GC,元空间增长
- 插件系统:独立 ClassLoader 隔离,父加载器共享接口
- Java 9 模块化:模块边界替代类加载器层次,网状依赖
面试必答点:
- 类加载五个阶段及各自作用(重点是准备阶段零值初始化)
- 主动使用与被动使用的区别(编译期常量、数组、父类字段)
<clinit>()的线程安全机制(饿汉式单例的原理)- 双亲委派的流程与好处(安全、唯一、有序)
- 类相等的条件(全限定名 + 类加载器)
- 自定义类加载器的实现(重写
findClass()) - JDBC 打破双亲委派的原因与方式(线程上下文类加载器)
- Tomcat 类加载体系与隔离原理(WebAppClassLoader 优先自己)
- 热部署原理与类加载器泄漏排查
- 插件系统的类加载器设计(隔离 + 通信)
- Java 9 模块化对类加载的影响(模块边界、网状依赖)