Время поговорить о создании новых классов во время выполнения в Groovy. Кажется, есть некоторый страх, неопределенность и сомнения по поводу утечек памяти и оценки кода с помощью Groovy в форме вызова eval()
метода.
Код, который, кажется, вызывает ужас:
def shell = new GroovyShell() 1000.times { shell.evaluate "x = 100" }
groovy.lang.GroovyShell
Экземпляр будет вызывать parseClass()
метод на внутренней groovy.lang.GroovyClassLoader
инстанции, которая создаст 1000 новых классов. Все классы будут расширять groovy.lang.Script
класс.
С каждым новым Class
созданием будет использоваться немного больше памяти. Пока groovy.lang.GroovyShell
экземпляр и, следовательно, его внутренний groovy.lang.GroovyClassLoader
экземпляр не являются сборщиком мусора, эта память будет оставаться занятой, даже если вы не сохраните ссылку на эти классы. Это стандартное ClassLoader
поведение Java .
Итак, как решить эту проблему? Что ж, ClassLoader
с Java немного сложно справиться, но это не так сложно, как только вы поймете, как они работают. Но давайте также рассмотрим корень проблемы, а именно тот факт, что Groovy создает новый Class
для каждого скрипта, который оценивается.
Прежде чем ответить, почему Groovy всегда создает новые Class
объекты при оценке кода, давайте сначала попробуем исправить приведенный выше код.
Один из способов это исправить:
def shell = new GroovyShell() 1000.times { shell.evaluate "x = 100" } shell = null
Установив shell
переменную в null
, будет ли GroovyClassLoader
экземпляр собирать мусор? Мы можем гарантировать, что это будет в этом кусочке кода. Но опять же этот код не делает ничего полезного ?
Вот еще один способ исправить это:
def shell = new GroovyShell() def script = shell.parse "x = 100" 1000.times { script.run() }
Оценивая — анализируя — код только один раз и вызывая run()
метод groovy.lang.Script
экземпляра 1000 раз, мы используем только 1/1000 памяти ? parse()
Метод возвращает groovy.lang.Script
экземпляр.
Но опять же, давайте рассмотрим более реалистичный вариант использования. В конце концов, статья, которая первоначально критиковала Groovy за утечки памяти, подразумевает, что оценка кода любое количество раз является действительным требованием в приложениях Entreprise.
Допустим, это скорее угловой случай, но, тем не менее, функциональность есть, и она может решать реальные проблемы. Оценка кода Groovy может быть особенно полезна и критична при оценке кода по требованию. Это может произойти, когда приложение считывает код из файла или базы данных для выполнения пользовательской бизнес-логики.
Давайте рассмотрим случай, когда разработчики создают DSL или домен-специфичный язык, например:
assert customer instanceof Customer assert invoice instanceof Invoice letterHead { customer { name = customer.name address { line1 = "${customer.streetName}, ${customer.streetNumber}" line2 = "${customer.zipCode} ${customer.location}, ${customer.state}" } } invoiceSummary { number = invoice.id creationDate = invoice.createdOn dueDate = invoice.payableOn } }
Для разбора этого DSL разработчики написали этот код (используя библиотеку iText PDF):
import com.lowagie.text.* import com.lowagie.text.pdf.* class LetterHeadFormatter { static byte[] createLetterHeadForInvoice(Customer cust, Invoice inv, String dsl) { Script dslScript = new GroovyShell().parse(dsl) dslScript.binding.variables.customer = cust dslScript.binding.variables.invoice = inv Document doc = new Document(PageSize.A4) def out = new ByteArrayOutputStream() PdfWriter writer = PdfWriter.getInstance(doc, out) doc.open() dslScript.metaClass = createEMC(writer, dslScript) dslScript.run() doc.close() return out.toByteArray() } static ExpandoMetaClass createEMC(PdfWriter writer, Script script) { ExpandoMetaClass emc = new ExpandoMetaClass(script.class, false) emc.letterHead = { Closure cl -> PdfContentByte content = writer.directContent cl.delegate = new LetterHeadDelegate(content) cl.resolveStrategy = Closure.DELEGATE_FIRST cl() } emc.initialize() return emc } }
(Проверьте приложения в этой статье, чтобы загрузить этот код. Прочтите README.txt
файл, если вы хотите запустить нагрузочное тестирование самостоятельно, и, пожалуйста, сообщите результаты. Также проверьте PDF-файл для вывода DSL.)
В строке 6 parse()
вызывается метод. Я написал нагрузочный тест, который вызывает createLetterHeadForInvoice()
метод 1 миллион (!) Раз (при регулярных вызовах System.gc()
). На моем компьютере с Windows XP, когда я запускаю нагрузочный тест с Ant, использование памяти процессом java колеблется между 32 и 37 МБ и остается стабильным в течение нескольких часов.
Собираются ли мусорные GroovyShell
и внутренние GroovyClassLoader
экземпляры? Да. Есть ли утечка памяти? Нет .
Так почему Groovy создает Class
es при оценке скриптов? Каждый бит кода в Groovy является java.lang.Class
. Это означает, что он загружается java.lang.ClassLoader
и остается в памяти, если не ClassLoader
может быть мусором. Почему Class
объектный мусор не собирается, как только он больше не используется? Почему ClassLoader
сам сборщик мусора должен быть удален перед удалением загруженных классов из памяти?
Если классы будут автоматически отбрасываться и перезагружаться, их статические переменные и статическая инициализация будут выполняться при каждой перезагрузке. Это было бы довольно удивительно и непредсказуемо. Вот почему ClassLoader
они должны держать свои классы, чтобы обеспечить предсказуемое поведение. Могут быть и другие технические причины, но это наиболее очевидная причина.
Как только сам ClassLoader
объект получает сборщик мусора (поскольку на него больше нет ссылок ни в одном стеке), сборщик мусора попытается выгрузить все свои Class
объекты.
Очевидно, что создание множества классов во время выполнения ClassLoader
увеличит использование памяти и, как правило, приведет к утечке памяти.
С другой стороны, поскольку каждый класс Groovy является настоящей Java Class
(без исключения), вам не нужно проводить различие.
В заключение: утечки памяти в GroovyShell
или нет GroovyClassLoader
. Загрузите образец кода и убедитесь сами. Ваш код может создать утечку памяти, используя ClassLoader
s — явно или неявно.