GroovyShell в Java8: утечка памяти / дублированные классы [предоставлен код src + нагрузочный тест]
У нас есть утечка памяти, вызванная скриптами GroovyShell/ Groovy (см. Код GroovyEvaluator в конце). Основные проблемы (копирование-вставка из анализатора MAT):
Класс "java.beans.ThreadGroupContext", загруженный "<системным загрузчиком классов>", занимает 807 406 960 (33,38%) байтов.
а также:
16 экземпляров "org.codehaus.groovy.reflection.ClassInfo$ClassInfoSet$ Segment", загруженных "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, которые мы используем: -XX: + CMSClassUnloadingEnabled -XX: + UseConcMarkSweepGC
Кстати, я прочитал https://dzone.com/articles/groovyshell-and-memory-leaks. Мы устанавливаем для оболочки GroovyShell значение null, когда она больше не нужна. Использование GroovyShell(). Parse(), вероятно, помогло бы, но для нас это на самом деле не вариант - у нас есть>10 наборов, каждый из которых состоит из 20-100 сценариев, и их можно изменить в любое время (во время выполнения).
Настройка MaxMetaspaceSize также должна помочь, но она не решает основную проблему, не устраняет основную причину. Так что я все еще пытаюсь зафиксировать это.
Я создал нагрузочный тест, чтобы воссоздать проблему (см. Код в конце). Когда я запускаю это:
- размер кучи, размер метапространства и количество классов продолжают расти
- дамп кучи, взятый через несколько минут, превышает 4 ГБ
Графики производительности за первые 3 минуты:
Как я уже упоминал, я использую MAT для анализа дампов кучи. Итак, давайте проверим отчет дерева Dominator:
Hashmap занимает> 30% кучи. Итак, давайте проанализируем это дальше. Посмотрим, что внутри него сидит. Давайте проверим записи хешей:
Сообщается 38 830 записей. Включая 38 780 записей с ключами, соответствующими ". Class Script. "
Еще одна вещь, отчет "дублирующиеся классы":
У нас есть 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;
}
}
Какие-нибудь мысли? Спасибо заранее
1 ответ
ОК, это мое решение:
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 МБ.
Также: больше нет необходимости использовать UseConcMarkSweepGC. Вы можете выбрать, какой GC вы хотите, или придерживаться по умолчанию:)
Синхронизация доступа к объектам скрипта может быть не лучшим вариантом, но я нашел единственный, который поддерживает размер Metaspace на приемлемом уровне. И даже лучше - это держит это постоянным. Еще. Это может быть не лучшим решением для всех, но отлично работает для нас. У нас есть большие наборы крошечных скриптов, что означает, что это решение (в значительной степени) масштабируемо.
Давайте посмотрим некоторые STATS для GroovyEvaluatorLoadTest с GroovyEvaluator, используя:
- старый подход с shell.evaluate(выражение):
0 итераций заняли 5.03 с 100 итераций заняли 285,185 с 200 итераций заняли 821,307 с
- script.setBinding(связывание):
0 итераций заняли 4.524 с 100 итераций заняли 19,291 с 200 итераций заняли 33,44 с 300 итераций заняли 47,791 с 400 итераций заняли 62,086 с 500 итераций заняли 77,329 с
Таким образом, дополнительное преимущество: оно молниеносно по сравнению с предыдущим, протекающим решением;)