Skip to content

类加载机制深度解析

面试官:说说 Java 的类加载机制吧,什么是双亲委派模型?

:双亲委派模型是 Java 类加载器的协作机制。当类加载器收到加载请求时,先委托父加载器,只有父加载器无法加载时才自己加载。这保证了核心类的安全性和唯一性。

面试官:那 Tomcat 为什么要破坏双亲委派模型?

这个问题考察的是对框架类加载机制的深入理解。能说清 Tomcat 的 WebAppClassLoader 和线程上下文类加载器的候选人,才是真正的 JVM 高手。


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>() 的特点:

  1. 编译器自动生成:收集所有类变量赋值和静态代码块,按源码顺序合并
  2. 父类优先:子类 <clinit>() 执行前,先执行父类的 <clinit>()
  3. 可选:如果没有静态变量和静态代码块,则不生成 <clinit>()
  4. 线程安全: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 保证线程安全,是饿汉式单例的底层原理。


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:如何判断两个类是否相等?为什么要有不同的类加载器?”

类相等的条件:

两个类相等,必须满足两个条件:

  1. 全限定名相同(包名 + 类名)
  2. 类加载器相同(必须是同一个 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:如何实现自定义类加载器?需要注意什么?”

自定义类加载器步骤:

  1. 继承 ClassLoader
  2. 重写 findClass() 方法(不要重写 loadClass(),破坏双亲委派)
  3. 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() 以保持双亲委派。


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.0
WebApp 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

排查方式:

Terminal window
# 1. 查看类加载数量
jstat -class <pid> 1000
# 2. 查看类加载器
jmap -clstats <pid>
# 3. 用 VisualVM 查看类加载数量是否持续增长
# 4. 用 MAT 分析 ClassLoader 引用链
# 找到谁在持有旧的 ClassLoader

防止泄漏的最佳实践:

1. 避免静态字段持有对插件类的引用
2. 热卸载前清理线程池(停止所有线程)
3. 注销所有监听器和回调
4. 使用 WeakReference 持有对插件类的引用
5. 定期重启应用(最简单粗暴)

本质一句话:热部署通过废弃旧类加载器实现,但类加载器泄漏会导致元空间增长,需清理所有引用。


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 ClassLoader

Java 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 模块化用模块边界替代类加载器层次,形成网状依赖,增强封装性,但核心仍是分层加载。


类加载核心知识点:

  1. 五个阶段:加载、验证、准备、解析、初始化
  2. 主动使用:6 种触发初始化的场景
  3. <clinit>():编译器自动生成,JVM 保证线程安全
  4. 双亲委派:先委托父加载器,保证安全性和唯一性
  5. 类相等:全限定名相同 + 类加载器相同
  6. 自定义类加载器:重写 findClass(),遵守双亲委派
  7. 线程上下文类加载器:解决 SPI 加载问题,双亲委派的”后门”
  8. Tomcat 类加载:WebAppClassLoader 优先加载自己,实现应用隔离
  9. 热部署:废弃旧类加载器,用新类加载器重新加载
  10. 类加载器泄漏:旧 ClassLoader 被持有无法 GC,元空间增长
  11. 插件系统:独立 ClassLoader 隔离,父加载器共享接口
  12. Java 9 模块化:模块边界替代类加载器层次,网状依赖

面试必答点:

  • 类加载五个阶段及各自作用(重点是准备阶段零值初始化)
  • 主动使用与被动使用的区别(编译期常量、数组、父类字段)
  • <clinit>() 的线程安全机制(饿汉式单例的原理)
  • 双亲委派的流程与好处(安全、唯一、有序)
  • 类相等的条件(全限定名 + 类加载器)
  • 自定义类加载器的实现(重写 findClass())
  • JDBC 打破双亲委派的原因与方式(线程上下文类加载器)
  • Tomcat 类加载体系与隔离原理(WebAppClassLoader 优先自己)
  • 热部署原理与类加载器泄漏排查
  • 插件系统的类加载器设计(隔离 + 通信)
  • Java 9 模块化对类加载的影响(模块边界、网状依赖)