Java8 中的 GroovyShell:内存泄漏/重复 类 [提供源代码 + 负载测试]
GroovyShell in Java8 : memory leak / duplicated classes [src code + load test provided]
我们有一个由 GroovyShell/Groovy 脚本引起的内存泄漏(请参阅最后的 Groovy 评估程序代码)。主要问题是(从 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 并不能解决问题。只是提高内存使用率 a little bit, 特别是。非堆。
Java 我们使用的参数:-XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC
顺便说一句,我读过 https://dzone.com/articles/groovyshell-and-memory-leaks。当不再需要时,我们确实将 GroovyShell shell 设置为空。使用 GroovyShell().parse() 可能会有所帮助,但这对我们来说并不是一个真正的选择——我们有 >10 套,每套由20-100 个脚本,可以随时更改(在 运行 时间)。
设置MaxMetaspaceSize 应该也有帮助,但它并没有真正解决根本问题,没有消除根本原因。所以我还在努力确定它。
我创建了负载测试来重现问题(见最后的代码)。当我 运行 它时:
- 堆大小、元空间大小和 classes 的数量不断增加
- 几分钟后进行的堆转储大于 4GB
前 3 分钟的性能图表:
正如我已经提到的,我正在使用 MAT 分析堆转储。那么让我们检查一下支配树报告:
Hashmap 占用 > 30% 的堆。
那么让我们进一步分析一下。让我们看看里面有什么。让我们检查哈希条目:
它报告了 38 830 个条目。包括 38 780 个键匹配 ".class Script."
的条目
另一件事,"duplicate classes"报告:
我们有 400 个条目(因为负载测试定义了 400 个 G.scripts),全部用于 "ScriptN" classes。
他们都持有对 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 的一些统计信息:
- 旧方法 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
所以额外的优势是:与以前的泄漏解决方案相比,它快如闪电 ;)
我们有一个由 GroovyShell/Groovy 脚本引起的内存泄漏(请参阅最后的 Groovy 评估程序代码)。主要问题是(从 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 并不能解决问题。只是提高内存使用率 a little bit, 特别是。非堆。
Java 我们使用的参数:-XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC
顺便说一句,我读过 https://dzone.com/articles/groovyshell-and-memory-leaks。当不再需要时,我们确实将 GroovyShell shell 设置为空。使用 GroovyShell().parse() 可能会有所帮助,但这对我们来说并不是一个真正的选择——我们有 >10 套,每套由20-100 个脚本,可以随时更改(在 运行 时间)。
设置MaxMetaspaceSize 应该也有帮助,但它并没有真正解决根本问题,没有消除根本原因。所以我还在努力确定它。
我创建了负载测试来重现问题(见最后的代码)。当我 运行 它时:
- 堆大小、元空间大小和 classes 的数量不断增加
- 几分钟后进行的堆转储大于 4GB
前 3 分钟的性能图表:
正如我已经提到的,我正在使用 MAT 分析堆转储。那么让我们检查一下支配树报告:
Hashmap 占用 > 30% 的堆。 那么让我们进一步分析一下。让我们看看里面有什么。让我们检查哈希条目:
它报告了 38 830 个条目。包括 38 780 个键匹配 ".class Script."
的条目另一件事,"duplicate classes"报告:
我们有 400 个条目(因为负载测试定义了 400 个 G.scripts),全部用于 "ScriptN" classes。 他们都持有对 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 的一些统计信息:
- 旧方法 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
所以额外的优势是:与以前的泄漏解决方案相比,它快如闪电 ;)