Статьи

GroovyShell и утечки памяти

Время поговорить о создании новых классов во время выполнения в 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 создает Classes при оценке скриптов? Каждый бит кода в Groovy является java.lang.Class. Это означает, что он загружается java.lang.ClassLoaderи остается в памяти, если не ClassLoaderможет быть мусором. Почему Classобъектный мусор не собирается, как только он больше не используется? Почему ClassLoaderсам сборщик мусора должен быть удален перед удалением загруженных классов из памяти?

Если классы будут автоматически отбрасываться и перезагружаться, их статические переменные и статическая инициализация будут выполняться при каждой перезагрузке. Это было бы довольно удивительно и непредсказуемо. Вот почему ClassLoaderони должны держать свои классы, чтобы обеспечить предсказуемое поведение. Могут быть и другие технические причины, но это наиболее очевидная причина.

Как только сам ClassLoaderобъект получает сборщик мусора (поскольку на него больше нет ссылок ни в одном стеке), сборщик мусора попытается выгрузить все свои Classобъекты.

Очевидно, что создание множества классов во время выполнения ClassLoaderувеличит использование памяти и, как правило, приведет к утечке памяти.

С другой стороны, поскольку каждый класс Groovy является настоящей Java Class(без исключения), вам не нужно проводить различие.

В заключение: утечки памяти в GroovyShellили нет GroovyClassLoader. Загрузите образец кода и убедитесь сами. Ваш код может создать утечку памяти, используя ClassLoaders — явно или неявно.