Groovy 4 GroovyClassLoader cache change: dynamic scripts still grow heap in long-running JVM
0 reputation · 15 Aug 2026, 20:14 UTC
0 reputation · 15 Aug 2026, 20:14 UTC
Groovy 4.0 documents that repeated dynamic compilation through fresh GroovyShell or GroovyClassLoader instances accumulates compiled classes in a static cache, which can leak memory in long-running JVM processes. The documented guidance is to reuse a single shared class loader or call GroovyClassLoader.clearCache() after use.
My goal is to run a service that compiles and executes user-supplied scripts continuously (thousands of compilations per day) without unbounded heap growth, while still isolating each script's classes so that recompiling a changed script picks up the new version. The uncertainty is how the cache interacts with loader reuse: a shared loader avoids the static-cache growth, but redefining the same script name appears to require either unique class names or a fresh loader, which reintroduces the original problem.
Environment assumption: Groovy 4.x on a modern HotSpot JVM, scripts compiled via GroovyShell.parse. I plan to verify by looping compilations and watching heap with and without clearCache().
Questions:
GroovyClassLoader, what is the supported way to recompile a script whose source changed without stale class reuse?clearCache() fully release compiled classes if the application still holds references to previously loaded script instances?35275 reputation · 16 Aug 2026, 06:27 UTC
The short answer: the cache is per-loader, not static, and clearCache() cannot unload classes. The durable fix for high-volume untrusted compilation is a short-lived child GroovyClassLoader per script version that you discard completely, not a shared loader with unique names.
A GroovyClassLoader keeps an instance-scoped cache keyed by class name. If you call GroovyShell.parse again with changed source but the same script class name, the loader returns the previously defined class — you get stale behavior, not a redefinition. There is no supported "replace this class" operation on a shared loader. Your two real options are:
CompilerConfiguration script base / naming strategy). This makes recompilation work, but every version stays loaded forever in that one loader — see the caveat below.No, not by itself. clearCache() empties the loader's internal name-to-class map, but JVM class unloading only happens when the defining ClassLoader itself becomes unreachable. If your application still holds script instances, Class objects, or — the classic trap — closures (which capture their defining class), the loader and every class it ever loaded stay alive. Groovy's metaclass/ClassInfo registry can also retain classes; reference strength there has changed across releases, so eviction behavior is version-sensitive within the 4.x line. Treat clearCache() as a hygiene step before discarding a loader, never as a substitute for discarding it.
The class cache is per-loader. The growth people attribute to a "static cache" usually comes from two other places: (a) accumulating loader instances, each of which pins all its classes, and (b) the shared metaclass registry. So creating a fresh GroovyShell per compilation leaks via loader accumulation; reusing one loader leaks via its ever-growing class table. Neither bounds memory on its own.
GroovyClassLoader.clearCache() on the child, and let the whole child loader be garbage-collected.Note that the pressure point is Metaspace as much as heap: every compiled script class consumes Metaspace, reclaimed only when its loader is collected. Unique names on a shared loader therefore just trade fast growth for slow single-loader growth — acceptable for a handful of trusted scripts, wrong for thousands of user-supplied ones.
Your loop-and-watch approach is right, but measure the correct things:
jcmd <pid> VM.classloader_stats or JFR), not just old-gen heap.-verbose:class or a heap dump that the script classes unloaded. If they didn't, walk the GC roots — a retained closure or ClassInfo entry is the usual culprit.One caveat: exact clearCache() semantics and ClassInfo reference strength vary across Groovy 4.x minor releases, so pin and check the version in your dependency tree before relying on any eviction behavior. Separately, compiling untrusted scripts raises sandboxing and resource-exhaustion concerns beyond memory that this answer does not cover.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.