memory-leaks - Java8 中的 GroovyShell : memory leak/duplicated classes [src code + load test provided]

标签 memory-leaks java-8 groovyshell groovyclassloader

我们遇到了由 GroovyShell/Groovy 脚本引起的内存泄漏(请参阅最后的 GroovyEvaluator 代码)。主要问题是(从 MAT 分析器复制粘贴):

The class "java.beans.ThreadGroupContext", loaded by "<system class loader>", occupies 807,406,960 (33.38%) bytes.



和:

16 instances of "org.codehaus.groovy.reflection.ClassInfo$ClassInfoSet$Segment", loaded by "sun.misc.Launcher$AppClassLoader @ 0x7004e9c80" occupy 1,510,256,544 (62.44%) bytes



我们正在使用 Groovy 2.3.11 和 Java8(准确地说是 1.8.0_25) .
升级到 Groovy 2.4.6 不能解决问题。只是稍微提高了内存使用率,尤其是。非堆。
我们正在使用的 Java 参数: -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC

顺便说一句,我读过 https://dzone.com/articles/groovyshell-and-memory-leaks .我们确实设置了 GroovyShell 不再需要时将 shell 设置为 null。使用 GroovyShell().parse() 可能会有所帮助,但这对我们来说并不是一个真正的选择 - 我们有 >10 个集合,每个集合包含 20-100 个脚本,并且可以随时更改它们(在运行时)。

设置最大元空间大小 也应该有帮助,但它并没有真正解决根本问题,也没有消除根本原因。所以我仍在努力确定它。

我创建了 负载测试重新创建问题(参见最后的代码)。当我运行它时:
  • 堆大小、元空间大小和类数不断增加
  • 几分钟后进行的堆转储大于 4GB

  • 前 3 分钟的性能图表:
    enter image description here

    正如我已经提到的,我正在使用 MAT 来分析堆转储。因此,让我们检查 Dominator 树报告:

    enter image description here

    Hashmap 占用 > 30% 的堆。
    那么让我们进一步分析一下。让我们看看里面有什么。让我们检查哈希条目:

    enter image description here

    它报告了 38 830 个条目。包括与“.class Script”匹配的键的 38 780 个条目。

    另一件事,“重复类(class)”报告:

    enter image description here

    我们有 400 个条目(因为负载测试定义了 400 个 G.scripts),全部用于“ScriptN”类。
    他们都持有对 groovyclassloader$innerloader 的引用

    我发现报告了类似的错误:https://issues.apache.org/jira/browse/GROOVY-7498 (见最后的评论和附上的截图)——他们的问题通过将 Java 升级到 1.8u51 得到了解决。不过,这对我们没有任何帮助。

    我们的代码:
    public class GroovyEvaluator
    {
        private GroovyShell shell;
    
        public GroovyEvaluator()
        {
            this(Collections.<String, Object>emptyMap());
        }
    
        public GroovyEvaluator(final Map<String, Object> contextVariables)
        {
            shell = new GroovyShell();
            for (Map.Entry<String, Object> contextVariable : contextVariables.entrySet())
            {
                shell.setVariable(contextVariable.getKey(), contextVariable.getValue());
            }
        }
    
        public void setVariables(final Map<String, Object> answers)
        {
            for (Map.Entry<String, Object> questionAndAnswer : answers.entrySet())
            {
                String questionId = questionAndAnswer.getKey();
                Object answer = questionAndAnswer.getValue();
                shell.setVariable(questionId, answer);
            }
        }
    
        public Object evaluateExpression(String expression)
        {
            return shell.evaluate(expression);
        }
    
        public void setVariable(final String name, final Object value)
        {
            shell.setVariable(name, value);
        }
    
        public void close()
        {
            shell = null;
        }
    }
    

    负载测试:
    /** Run using -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC */
    public class GroovyEvaluatorLoadTest
    {
        private static int NUMBER_OF_QUESTIONS = 400;
        private final Map<String, Object> contextVariables = Collections.emptyMap();
        private List<Fact> factMappings = new ArrayList<>();
    
        public GroovyEvaluatorLoadTest()
        {
            for (int i=0; i<NUMBER_OF_QUESTIONS; i++)
            {
                factMappings.add(new Fact("fact" + i, "question" + i));
            }
        }
    
        private void callEvaluateExpression(int iter)
        {
            GroovyEvaluator groovyEvaluator = new GroovyEvaluator(contextVariables);
    
            Map<String, Object> factValues = new HashMap<>();
            Map<String, Object> answers = new HashMap<>();
            for (int i=0; i<NUMBER_OF_QUESTIONS; i++)
            {
                factValues.put("fact" + i, iter + "-fact-value-" + i);
                answers.put("question" + i, iter + "-answer-" + i);
            }
    
            groovyEvaluator.setVariables(answers);
            groovyEvaluator.setVariable("answers", answers);
            groovyEvaluator.setVariable("facts", factValues);
    
            for (Fact fact : factMappings)
            {
                groovyEvaluator.evaluateExpression(fact.mapping);
            }
            groovyEvaluator.close();
        }
    
        public static void main(String [] args)
        {
            GroovyEvaluatorLoadTest test = new GroovyEvaluatorLoadTest();
    
            for (int i=0; i<995000; i++)
            {
                test.callEvaluateExpression(i);
            }
            test.callEvaluateExpression(0);
        }
    }
    
    public class Fact
    {
        public final String factId;
    
        public final String mapping;
    
        public Fact(final String factId, final String mapping)
        {
            this.factId = factId;
            this.mapping = mapping;
        }
    }
    

    有什么想法吗?
    提前谢谢

    最佳答案

    好的,这是我的解决方案:

    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;
        }
    }
    

    新解决方案保留加载类的数量和元数据大小在一个常数级别 .非堆分配的内存使用量 = ~70 MB。

    另外:不再需要使用 UseConcMarkSweepGC。您可以选择您想要的任何 GC 或坚持使用默认的 :)

    同步对脚本对象的访问可能不是最好的选择,但我发现这是唯一一种将元空间大小保持在合理水平的方法。甚至更好 - 它保持不变。仍然。它可能不是每个人的最佳解决方案,但对我们很有用。我们有大量的小脚本,这意味着这个解决方案(几乎)是可扩展的。

    让我们使用 GroovyEvaluator 查看 GroovyEvaluatorLoadTest 的一些 STATS:
  • 使用 shell.evaluate(expression) 的旧方法:

  • 0 次迭代耗时 5.03 秒
    100 次迭代耗时 285.185 秒
    200 次迭代耗时 821.307 秒
  • script.setBinding(绑定(bind)):

  • 0 次迭代耗时 4.524 秒
    100 次迭代耗时 19.291 秒
    200 次迭代耗时 33.44 秒
    300 次迭代耗时 47.791 秒
    400 次迭代耗时 62.086 秒
    500 次迭代耗时 77.329 秒

    所以额外的优势是:与以前的泄漏解决方案相比,它快如闪电;)

    关于memory-leaks - Java8 中的 GroovyShell : memory leak/duplicated classes [src code + load test provided],我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36407119/

    相关文章:

    java - Java版本之间的不兼容性

    java - 如何从 Java 评估我自己的 Groovy 脚本?

    groovy - 如何将多个jar添加到groovyConole/groovysh的类路径中?

    java - 使用 HeapDumpOnOutOfMemoryError 参数进行 JBoss 的堆转储

    c++ - 没有动态内存的内存泄漏

    memory-leaks - Xamarin.iOS 简单 NavigationController 内存泄漏

    java - 获取有关特定内存泄漏的更多详细信息

    Java 8 Lambdas 和并发解释

    java - 为什么 javac 需要引用类的接口(interface)而 ECJ 不需要?

    grails - Grails 中的运行时脚本评估 - 最佳实践