核心是复用groovyclassloader与预编译脚本类,禁用其内置缓存,以sha-256哈希为key缓存class(推荐weakreference),避免频繁创建groovyshell;确保脚本不强引用外部对象、不继承应用非接口类型,并配合jvm参数与工具验证类卸载效果。

核心思路是:不让每次执行都生成新类,而是复用已编译的脚本类,并确保类加载器能被及时回收。
复用 GroovyClassLoader 实例,禁用内部缓存
GroovyClassLoader 默认会缓存所有 parseClass 生成的 Class,且自身不自动释放。正确做法是自己管理一个线程安全的脚本类缓存,同时关闭其内置缓存:
- 创建单例或 Spring 管理的 GroovyClassLoader,设置 parent = Thread.currentThread().getContextClassLoader(),避免与应用类加载器隔离
- 调用 loader.setShouldRecompile(false) 和 loader.clearCache() 防止重复解析同一脚本
- 用脚本内容的 SHA-256 哈希作 key,缓存编译后的 Class 对象(WeakReference 更稳妥)
避免每次 new GroovyShell —— 改用预编译 + Binding 复用
GroovyShell 每次实例化都会新建 ClassLoader 和 AST 解析器,是 Metaspace 暴涨的直接推手:
- 不要在 evaluate 前 new GroovyShell;改为用已缓存的 Class.newInstance() 创建实例
- Binding 对象可复用,但需注意:Binding 是有状态的,每次执行前调用 binding.clear() 并重新 put 参数
- 若必须用 Shell,至少将 GroovyShell 实例池化(例如 Guava Cache),按 script hash 缓存,超时自动清理
强制类卸载的关键控制点
即使缓存了 Class,若其 ClassLoader 无法被 GC,元空间仍不会释放:
- 确保脚本中不持有外部对象的强引用(如 static 字段、线程局部变量、未关闭的资源)
- 脚本类不能继承或实现应用中定义的非接口类型(否则会强依赖应用类加载器)
- 定期触发元空间 GC:可通过 -XX:+UseG1GC -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m 控制阈值,并配合 -XX:+PrintGCDetails 观察 Metaspace Unloading 日志
上线前必做的验证动作
光改代码不够,得用工具确认是否真正生效:
- 启动时加 -XX:+TraceClassLoading -XX:+TraceClassUnloading,观察日志中是否有对应脚本类的 unload 记录
- 用 jcmd $PID VM.classloader_stats 查看已加载类数和 ClassLoader 实例数,反复执行同一脚本后二者应基本稳定
- 通过 jstat -gcmetacapacity $PID 监控 MU(Metaspace 使用量),持续运行 1 小时后增长幅度应










