在何种情况下,元空间中的类信息会被自动回收,例如当自定义类加载器被卸载?
- 内容介绍
- 相关推荐
本文共计902个文字,预计阅读时间需要4分钟。
Java中类卸载不是自动发生的常规操作,而是一套严格的依赖+GC+可达性判定的被动机制。在Metaspace(元空间)中的类信息,只有在满足全部硬性条件时,才可能在Full GC期间被真正回收。
三个必须同时成立的卸载前提
缺一不可,任一条件不满足,类就“卡”在 Metaspace 里不动:
-
该类所有 Java 堆实例已被 GC 回收:包括直接 new 出的对象、数组元素、内部类隐式持有的外部类实例等;静态集合中残留的引用(如
static List<MyService> cache)会阻止实例被清空 -
加载它的 ClassLoader 实例已被 GC 回收:ClassLoader 是类元数据的“主人”,Metaspace 按加载器隔离存储;只要它被任何静态字段、线程上下文(
Thread.currentThread().getContextClassLoader())、第三方框架(如 Spring 的上下文持有)间接引用,就无法回收 -
该类对应的
Class对象无任何强引用:常见泄漏点包括:static Map<String, Class>缓存、ThreadLocal<Class>未调用remove()、反射调用后残留的Method.setAccessible(true)、JNI 全局引用(NewGlobalRef未配对DeleteGlobalRef)
自定义类加载器被回收是关键突破口
标准系统类加载器(如 AppClassLoader)生命周期与 JVM 一致,基本不会被回收;而动态场景(热部署、插件化、Web 应用重启)中,自定义类加载器才是可回收对象的主力:
- Tomcat 的
WebAppClassLoader在应用停用时若未被 Servlet 容器彻底清理(比如线程池未关闭、监听器未释放),就会持续持有所加载的所有类 - OSGi 或 Spring Boot DevTools 热加载时,新旧 ClassLoader 切换失败,旧加载器残留,导致整批类元数据堆积
- 手动使用
URLClassLoader加载插件后,必须显式置空所有强引用,并确保线程上下文类加载器已重置为null
元空间回收不等于类卸载,也不等于还给操作系统
即使类成功卸载,Metaspace 内存也不会立刻归还 OS:
- 卸载后,对应元数据内存块进入 JVM 内部空闲池,优先复用于后续新类加载
- 只有触发 Full GC(如 CMS 或 G1 的并发标记完成阶段),且空闲率超过
-XX:MaxMetaspaceFreeRatio(默认 70%),才会尝试收缩并返还大块连续内存 -
-XX:MaxMetaspaceSize设得太小反而引发频繁 GC,增加开销;建议结合实际负载设置合理上限(如 512m–1g)
怎么确认类真的卸载了?
别只看 jstat -gc <pid> 的 MU 下降——那可能是加载失败回滚或临时元数据清理:
- 加启动参数
-XX:+TraceClassUnloading,运行中看到类似[Unloading class com.example.PluginService]的日志才算实锤 - 配合
-XX:+CMSClassUnloadingEnabled(CMS)或-XX:+G1UseConcMarkSweepGC(G1,JDK 8u40+)启用 GC 策略级类卸载支持 - JFR(Java Flight Recorder)中开启
jdk.ClassUnloading事件,比日志更稳定、低开销
本文共计902个文字,预计阅读时间需要4分钟。
Java中类卸载不是自动发生的常规操作,而是一套严格的依赖+GC+可达性判定的被动机制。在Metaspace(元空间)中的类信息,只有在满足全部硬性条件时,才可能在Full GC期间被真正回收。
三个必须同时成立的卸载前提
缺一不可,任一条件不满足,类就“卡”在 Metaspace 里不动:
-
该类所有 Java 堆实例已被 GC 回收:包括直接 new 出的对象、数组元素、内部类隐式持有的外部类实例等;静态集合中残留的引用(如
static List<MyService> cache)会阻止实例被清空 -
加载它的 ClassLoader 实例已被 GC 回收:ClassLoader 是类元数据的“主人”,Metaspace 按加载器隔离存储;只要它被任何静态字段、线程上下文(
Thread.currentThread().getContextClassLoader())、第三方框架(如 Spring 的上下文持有)间接引用,就无法回收 -
该类对应的
Class对象无任何强引用:常见泄漏点包括:static Map<String, Class>缓存、ThreadLocal<Class>未调用remove()、反射调用后残留的Method.setAccessible(true)、JNI 全局引用(NewGlobalRef未配对DeleteGlobalRef)
自定义类加载器被回收是关键突破口
标准系统类加载器(如 AppClassLoader)生命周期与 JVM 一致,基本不会被回收;而动态场景(热部署、插件化、Web 应用重启)中,自定义类加载器才是可回收对象的主力:
- Tomcat 的
WebAppClassLoader在应用停用时若未被 Servlet 容器彻底清理(比如线程池未关闭、监听器未释放),就会持续持有所加载的所有类 - OSGi 或 Spring Boot DevTools 热加载时,新旧 ClassLoader 切换失败,旧加载器残留,导致整批类元数据堆积
- 手动使用
URLClassLoader加载插件后,必须显式置空所有强引用,并确保线程上下文类加载器已重置为null
元空间回收不等于类卸载,也不等于还给操作系统
即使类成功卸载,Metaspace 内存也不会立刻归还 OS:
- 卸载后,对应元数据内存块进入 JVM 内部空闲池,优先复用于后续新类加载
- 只有触发 Full GC(如 CMS 或 G1 的并发标记完成阶段),且空闲率超过
-XX:MaxMetaspaceFreeRatio(默认 70%),才会尝试收缩并返还大块连续内存 -
-XX:MaxMetaspaceSize设得太小反而引发频繁 GC,增加开销;建议结合实际负载设置合理上限(如 512m–1g)
怎么确认类真的卸载了?
别只看 jstat -gc <pid> 的 MU 下降——那可能是加载失败回滚或临时元数据清理:
- 加启动参数
-XX:+TraceClassUnloading,运行中看到类似[Unloading class com.example.PluginService]的日志才算实锤 - 配合
-XX:+CMSClassUnloadingEnabled(CMS)或-XX:+G1UseConcMarkSweepGC(G1,JDK 8u40+)启用 GC 策略级类卸载支持 - JFR(Java Flight Recorder)中开启
jdk.ClassUnloading事件,比日志更稳定、低开销

