Java8中的GroovyShell:内存泄漏/重复类[src代码+提供的负载测试]

时间:2016-04-04 15:42:12

标签: memory-leaks java-8 groovyshell groovyclassloader

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

  

类“java.beans.ThreadGroupContext”,由“< system class”加载   loader>“,占用807,406,960(33.38%)字节。

  

16个实例   “org.codehaus.groovy.reflection.ClassInfo $ ClassInfoSet $段”,   加载“sun.misc.Launcher $ AppClassLoader @ 0x7004e9c80”占用   1,510,256,544(62.44%)字节

我们正在使用 Groovy 2.3.11和Java8(准确地说是1.8.0_25)
升级到 Groovy 2.4.6 并不能解决问题。只需提高内存使用量一个 ,尤其是非堆。
我们正在使用的Java args: -XX:+ CMSClassUnloadingEnabled -XX:+ UseConcMarkSweepGC

BTW,我读过https://dzone.com/articles/groovyshell-and-memory-leaks。我们确实将 GroovyShell shell设置为null,当它不再需要时。使用 GroovyShell()。parse()可能会有所帮助,但对我们来说它实际上并不是一个选项 - 我们有> 10套,每套包含20-100个脚本,并且它们可以在任何时候(在运行时)。

设置 MaxMetaspaceSize 也应该有所帮助,但它并没有真正解决根本问题,也没有消除根本原因。所以我仍然试图把它钉死。

我创建了加载测试来重新创建问题(请参阅最后的代码)。当我运行它时:

  • 堆大小,元空间大小和类数量不断增加
  • 几分钟后的堆转储大于4GB

前3分钟的效果图表: enter image description here

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

enter image description here

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

enter image description here

它报告了38 830个酒。包括38 780个条目,其键与“。 class Script。

匹配

另一件事,“重复课程”报告:

enter image description here

我们有400个条目(因为加载测试定义了400个G脚本),全部用于“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;
    }
}

有什么想法? Thx提前

1 个答案:

答案 0 :(得分:4)

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

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或坚持使用默认的GC:)

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

让我们使用GroovyEvaluator查看GroovyEvaluatorLoadTest的一些STATS:

  • 使用shell.evaluate(表达式)的旧方法:
0 iterations took 5.03 s
100 iterations took 285.185 s
200 iterations took 821.307 s
  • script.setBinding(结合):
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

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