-XX:+ClassUnloading 仅允许JVM在ClassLoader不可达等条件满足时卸载类,非强制回收;根本在于ClassLoader及其引用链未被GC,需显式清理缓存、关闭ClassLoader并触发GC验证卸载效果。

ClassUnloading 参数本身不提供“强制回收”能力
Java 的 -XX:+ClassUnloading(或默认开启的 -XX:+UseConcMarkSweepGC 时代遗留配置)只是**允许** JVM 在满足条件时卸载无用的类,它不是开关式指令。在动态脚本引擎(如 Groovy、Janino、Jython 或自研 ClassLoader 封装)中,即使开启了该参数,过期类仍大概率残留——根本原因在于类加载器(ClassLoader)未被 GC 回收,导致其加载的所有类(包括 Class 对象和静态字段)强引用链持续存在。
真正要做的:切断 ClassLoader 引用并触发 Full GC
动态脚本引擎每次编译新脚本,通常会创建一个新 URLClassLoader 或子类。若旧脚本下线后,仍有以下任一情况,类就无法卸载:
- 线程局部变量(
ThreadLocal)持有该类或其实例 - 静态集合(如
Map<string object></string>)缓存了该类的实例或Class对象 - 日志框架(如 Log4j)的 MDC/NDC 中存有该类相关上下文
- 该
ClassLoader被任意活跃对象(比如监听器、回调函数闭包)间接引用
实操建议:
- 显式调用
classLoader.close()(若为URLClassLoader且 JDK ≥ 7) - 清空所有已知静态缓存,尤其是
ConcurrentHashMap类型的类名 →Class映射表 - 避免在脚本类中使用
static字段;改用依赖注入或上下文传参 - 用
jcmd <pid> VM.class_hierarchy -all</pid>或jmap -clstats <pid></pid>观察类加载器存活数量变化
为什么 -XX:+ClassUnloading 单独开启没用
该参数仅影响 CMS/G1 等垃圾收集器是否尝试处理 java.lang.Class 对象的可达性判断。但它的前提成立需要:
-
ClassLoader实例本身不可达(即无任何强引用) - 该
ClassLoader加载的所有类均无活跃实例,且无静态字段引用外部对象 - 没有 JNI 全局引用指向这些类(常见于嵌入式脚本引擎的 native 层封装)
典型失败场景:GroovyShell 默认复用 GroovyClassLoader,而它内部持有一个 ConcurrentHashMap<string class></string> 缓存——哪怕你 new 了一个新 shell,旧缓存仍在,旧类 loader 就不会被回收。
可验证的最小闭环操作步骤
以 Janino 为例,在每次脚本更新后执行:
- 销毁旧
SimpleCompiler实例(它内部封装了ClassLoader) - 手动置空你维护的脚本类名 →
Class映射:scriptClassCache.remove(scriptId) - 确保无线程正在执行该脚本的旧
Method(必要时加同步或中断线程) - 主动触发一次
System.gc()(仅用于调试;生产环境靠自然 GC 周期) - 几秒后用
jstat -gc <pid></pid>查看MC(元空间容量)是否下降,再用jmap -histo:live <pid> | grep YourScriptClassName</pid>确认类是否消失
注意:元空间(Metaspace)不会立即归还给操作系统,MC 下降 ≠ 内存释放,要看 MU(已使用元空间)是否回落。

















