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?