核心是复用已编译脚本类并及时回收类加载器:使用单例groovyclassloader,禁用缓存、按sha-256哈希缓存class(weakreference包装),避免新建groovyshell,切断脚本对应用类的强引用,并通过jvm参数和工具验证类卸载效果。

核心是不让每次执行都生成新类,而是复用已编译的脚本类,并确保对应的类加载器能被及时回收。
复用 GroovyClassLoader 并关闭其内置缓存
默认的 GroovyClassLoader 会缓存所有 parseClass 结果,且自身不自动释放,容易造成类加载器泄漏:
- 使用单例或 Spring 管理的 GroovyClassLoader 实例,设置
parent = Thread.currentThread().getContextClassLoader(),避免与应用类加载器隔离 - 调用
loader.setShouldRecompile(false)和loader.clearCache(),防止重复解析同一脚本 - 以脚本内容的 SHA-256 哈希值为 key,缓存编译后的
Class对象;推荐用WeakReference<class></class>包装,降低内存持有风险
避免频繁创建 GroovyShell
GroovyShell 每次实例化都会新建独立的类加载器和 AST 解析器,是元空间暴涨的直接诱因:
- 不要在
evaluate()前new GroovyShell();改用预编译后的Class.newInstance()创建脚本实例 -
Binding 可复用,但每次执行前必须调用
binding.clear(),再重新put参数,避免状态污染 - 若业务强依赖 Shell 接口,至少将
GroovyShell实例按 script hash 池化(如 Guava Cache),并设合理超时自动清理
切断脚本对应用类的强引用
即使 Class 被缓存,只要其类加载器无法被 GC,元空间就不会真正释放:
- 脚本中禁止使用
static字段、ThreadLocal或未关闭的资源(如数据库连接、文件句柄)持有外部对象 - 脚本类不能继承或实现应用中定义的非接口类型(例如不能
extends ServiceImpl或implements OrderService),否则会强绑定应用类加载器 - JVM 启动参数建议加上:
-XX:+UseG1GC -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:+PrintGCDetails,配合日志观察Metaspace Unloading是否发生
上线前必须验证类是否真正卸载
改完代码不等于问题消失,得用工具确认效果:
- 加 JVM 参数
-XX:+TraceClassLoading -XX:+TraceClassUnloading(测试环境),检查日志中是否有对应脚本类的unloaded记录 - 反复执行同一脚本后,运行
jcmd $PID VM.classloader_stats,确认loadedClassCount和numClassLoaderInstances不再持续增长 - 用
jstat -gcmetacapacity $PID 2s监控MU(Metaspace used),稳定运行 1 小时后增幅应趋近于零










