GroovyShell in Java8 : memory leak / duplicated classes [src code + load test provided]

后端 未结 1 1024
我寻月下人不归
我寻月下人不归 2021-02-06 13:56

We have a memory leak caused by GroovyShell/ Groovy scripts (see GroovyEvaluator code at the end). Main problems are (copy-paste from MAT analyser):

The c

相关标签:
1条回答
  • 2021-02-06 14:05

    OK, this is my solution:

    public class GroovyEvaluator
    {
        private static GroovyScriptCachingBuilder groovyScriptCachingBuilder = new GroovyScriptCachingBuilder();
        private Map<String, Object> variables = new HashMap<>();
    
        public GroovyEvaluator()
        {
            this(Collections.<String, Object>emptyMap());
        }
    
        public GroovyEvaluator(final Map<String, Object> contextVariables)
        {
            variables.putAll(contextVariables);
        }
    
        public void setVariables(final Map<String, Object> answers)
        {
            variables.putAll(answers);
        }
    
        public void setVariable(final String name, final Object value)
        {
            variables.put(name, value);
        }
    
        public Object evaluateExpression(String expression)
        {
            final Binding binding = new Binding();
            for (Map.Entry<String, Object> varEntry : variables.entrySet())
            {
                binding.setProperty(varEntry.getKey(), varEntry.getValue());
            }
            Script script = groovyScriptCachingBuilder.getScript(expression);
            synchronized (script)
            {
                script.setBinding(binding);
                return script.run();
            }
        }
    
    }
    
    public class GroovyScriptCachingBuilder
    {
        private GroovyShell shell = new GroovyShell();
        private Map<String, Script> scripts = new HashMap<>();
    
        public Script getScript(final String expression)
        {
            Script script;
            if (scripts.containsKey(expression))
            {
                script = scripts.get(expression);
            }
            else
            {
                script = shell.parse(expression);
                scripts.put(expression, script);
            }
            return script;
        }
    }
    

    New solution keeps number of loaded classes and Metadata size at a constant level. Non-heap allocated memory usage = ~70 MB.

    Also: there is no need to use UseConcMarkSweepGC anymore. You can choose whichever GC you want or stick with a default one :)

    Synchronising access to script objects might not the best option, but the only one I found that keeps Metaspace size within reasonable level. And even better - it keeps it constant. Still. It might not be the best solution for everyone but works great for us. We have big sets of tiny scripts which means this solution is (pretty much) scalable.

    Let's see some STATS for GroovyEvaluatorLoadTest with GroovyEvaluator using:

    • old approach with shell.evaluate(expression):
    0 iterations took 5.03 s
    100 iterations took 285.185 s
    200 iterations took 821.307 s
    
    • script.setBinding(binding):
    0 iterations took 4.524 s
    100 iterations took 19.291 s
    200 iterations took 33.44 s
    300 iterations took 47.791 s
    400 iterations took 62.086 s
    500 iterations took 77.329 s
    

    So additional advantage is: it's lightning fast compared to previous, leaking solution ;)

    0 讨论(0)
提交回复
热议问题