My clojure application evaluates code defined in separate .edn files at runtime, i.e. if the .edn files are changed, the contained function definitions are reloaded into an atom, which is constantly used for calculations.
The application seems to fill up the JVM's metaspace (without bounds) after a while if it's not limited. Heap space usage is normal.
I used the "YourKit" profiler to track down the leaks. It seems that most allocations, including those where the profiler says "unreachable from Garbage Collector roots", come from the eval call:
(defn eval-edn [e params_ dynparams_]
(let [input e
pairs (seq input)]
(binding [*ns* (find-ns 'myapp.core)
moods (:moods input)
last-moods @state/moods-atom
effects (:effects input)
last-effects @state/effects-atom
params params_
dynparams dynparams_
param-stats @state/coreparam-stats]
(eval input))))
Where params and dynparams are dynamic vars.
The .edn file looks like this:
{:moods {:happiness
(* (:happiness:factor dynparams)
(* 0.5 (Math/sin (* (/ (- (:weather:temperature params 0) 10) 30)
Math/PI))))
; ...
}}
Specifically, the memory snapshot will show thousands of objects of type myapp.core$evalN.invoke(), where N is some index increased for every new call.
I've read that eval should be avoided where possible, but I don't see how I can achieve reloading code at runtime in a JAR executable without it.
How can I make sure all the memory used by eval is cleaned up correctly?