Статьи

Java.lang.OutOfMemory: пространство PermGen — сборщик мусора при загрузке классов

Недавно мы столкнулись с проблемой «java.lang.OutOfMemory: пространство PermGen». Этот опыт стал настоящим откровением. В SMART мы используем изоляцию Classloader для изоляции нескольких арендаторов. Мы начали тестировать его в бета-версии несколько недель назад и обнаружили, что каждые 2 дня наш сервер отключался, предоставляя пространство OutOfMemory: PermGen. У нас было 6 арендаторов с небольшим или очень небольшим объемом данных. Это было очень тревожно, так как это начало происходить, когда к серверу не было доступа и нагрузка на него была очень низкой. Для нас было очевидно, что утечка была у классных загрузчиков. Но вопрос был в том, почему он не собирал мусор? Подводя итог нашим выводам, нужно было исправить следующее, прежде чем мы сможем заставить загрузчик классов собирать мусор:

  • Очистка кеша JCS
  • Solr Searcher темы
  • Статические переменные
  • Темы и Пулы

Отслеживание утечек

Прежде чем я подробно расскажу о каждом из пунктов в приведенном выше списке, позвольте мне рассказать, как мы обнаружили эти утечки. Основная проблема в отслеживании и устранении утечек памяти заключается в повторном создании проблемы. Если вы можете воссоздать его, то половина проблемы решена. Нам потребовалось некоторое время и много нестандартного мышления, но мы смогли точно определить набор шагов, которые необходимо предпринять для воссоздания проблемы. Нам пришлось удалить арендаторов из кеша JCS и снова загрузить их в кеш. Сделайте это 4 или 5 раз, и мы сможем воссоздать проблему OutOfMemory. Когда мы впервые начали видеть эту проблему, мы добавили стандартные параметры java для выгрузки кучи, когда процессу не хватило памяти.

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapDumps

Это помогло нам указать на то, что ClassLoader не является сборщиком мусора в качестве проблемы. Но дамп происходил каждые 2 дня и нерегулярно в зависимости от того, как использовался сервер, и каждый раз, когда вносилось исправление, нам приходилось ждать два дня, чтобы увидеть, действительно ли исправление сработало. Первый урок, который вам не нужно исчерпывать памяти для сброса кучи. Очень полезным инструментом, который помогает в анализе кучи, является jmap, поставляемый с jdk 6. Две действительно полезные команды, использующие jmap:

jmap -permstat <pid>

Эта команда показывает загрузчики классов в пространстве perm gen и их статус, если они живы или мертвы. Это очень полезно, чтобы проверить, были ли загрузчики классов собраны мусором.

jmap -dump:format=b,file=heap.bin <pid>

Это выдает кучу в файл heap.bin, а затем может быть исследован, чтобы найти причину, по которой загрузчик классов не собирает мусор. Мы использовали инструмент под названием visualvm, еще один очень полезный инструмент. Это помогает просмотреть выгруженную кучу и может показать ближайший корень GC, который содержит объект в куче, чтобы предотвратить его сборку мусора. С этими инструментами он стал итеративным процессом:

  • Воссоздать проблему с одним арендатором

    • Мы сократили время ожидания JCS до 2 минут.
    • Арендатор теперь удаляется из кэша каждые 2 минуты
  • Дамп кучи с помощью команды jmap
  • Изучите кучу с помощью Visual VM
  • Найдите объект, удерживающий загрузчик классов, и устраните утечку.

Очистка кеша JCS

Основной проблемой, с которой мы столкнулись, когда мы начали исправлять эту проблему, было «Как мы фиксируем событие удаления клиента из кэша JCS и добавляем в него нашу собственную обработку?». Одним из предложений для этого является:

     JCS jcs = JCSCacheFactory.getCacheInstance(regionName);
     IElementAttributes attributes = jcs.getDefaultElementAttributes();
     attributes.addElementEventHandler(handler);
     jcs.setDefaultElementAttributes(attributes);

Но установка этого способа не изменила атрибуты. Но отсюда ясно, что

  1. Были события, которые были вызваны для каждого объекта в кеше jcs
  2. Атрибуты элемента имели обработчик для этих событий

Нам просто нужно было найти способ заменить этот обработчик событий. ElementAttributes фактически настраиваются в cache.ccf. Все, что нам нужно было сделать, это заменить конфигурацию нашим собственным классом атрибутов элемента. Мы создали класс TenantMemoryAttributes, который сделал следующее:

public class TenantMemoryAttributes extends ElementAttributes implements java.io.Serializable
{
    public class CacheObjectCleanup implements IElementEventHandler
    {
        public void handleElementEvent(IElementEvent event)
        {
                CacheElement elem = (CacheElement)(((ElementEvent)event).getSource());
                if (elem != null)
                {
                    SmartTenant tenant = (SmartTenant)elem.getVal();
                    .... cleanup here ...
                }
        }
    }
    public TenantMemoryAttributes()
    {
        super();
        System.out.println("TenantMemoryAttributes is instantiated and new handler registered.");
        addElementEventHandler(new CacheObjectCleanup());
    }
}

Как только у нас появился этот класс, нам просто нужно было добавить его в cache.ccf.

jcs.region.TenantsHosted.elementattributes=org.anon.smart.base.tenant.TenantMemoryAttributes

Теперь у нас был крюк для очистки, когда арендатор был удален из памяти. Имея это в виду, мы обнаружили, что мы еще не закончили с кешем JCS. Хотя мы вызвали функцию JCS.clear (), чтобы очистить все элементы в кэше JCS (данные, используемые арендатором), для освобождения загрузчика класса необходимо было сделать и другие вещи. CompositeCacheManager — это одиночный и имеет статическую переменную «экземпляр». Вызов clear только очистил данные, но экземпляр все еще содержал класс CompositeCache. Итак, нам пришлось вызвать freeCache на менеджере, поэтому все ссылки на CompositeCache были освобождены. Как только это было сделано, мы поняли, что существует поток процессора очереди событий, который отправляет события, и этот поток не останавливается. Чтобы сделать оба эти, мы должны были позвонить:

CompositeCacheManager.getInstance().freeCache(_name);
CompositeCache.elementEventQ.destroy();

Это гарантировало, что кеш JCS больше не был узким местом, содержащим загрузчик классов.

Solr Searcher

Мы используем встроенный solr в SMART для индексации и обеспечения текстового поиска по сохраненным данным. Это начало вызывать следующее узкое место, чтобы освободить загрузчик классов. Класс SolrCore представляет каждое ядро ​​в solr и содержит пул потоков с именем searcherExecutor. Классы solr загружаются загрузчиком в SMART. Мы ожидали, что, поскольку эти классы загружаются загрузчиком классов начальной загрузки, Solr не должен препятствовать загрузке нашего загрузчика классов. Здесь мы были не правы, и причина глубоко связана с тем, как работают потоки Java и безопасность, и было трудно найти и исправить. В загрузчике классов Java, когда класс определяется с помощью вызова, как показано ниже:

defineClass(className, classBytes, 0, classBytes.length, null); -- note the null for the domain

Java создает DefaultDomain по умолчанию следующим образом (см. ClassLoader.java в rt.jar):

      this.defaultDomain = new ProtectionDomain(localCodeSource, null, this, null); 
......

Этот домен по умолчанию затем используется в качестве домена защиты для классов, которые загружаются с нулевым доменом. Само по себе это просто круговая ссылка, и на самом деле это не должно было вызывать никаких проблем Тем не менее, когда это в сочетании с безопасностью потока это стало очень важно, чтобы не выпускать наш загрузчик классов. Нити в Java содержат то, что называется «наследованным доступом AccessControlContext». Из этой статьи о безопасности Java :

Когда создается новый поток, мы фактически гарантируем (посредством создания потока и другого кода), что он автоматически наследует контекст безопасности родительского потока во время создания дочернего потока таким образом, что последующие вызовы checkPermission в дочернем потоке будут принять во внимание наследуемый родительский контекст.

Это отражено в коде для создания потока. Проверьте код в Thread.java из rt.jar. У метода init есть этот кусок кода.

    this.inheritedAccessControlContext = AccessController.getContext();

И getContext в AccessController имеет этот кусок кода:

    AccessControlContext localAccessControlContext = getStackAccessControlContext();

Теперь это кажется правильным, логичным и безвредным. Тем не менее, у нас есть последовательность, как показано ниже:

my Class Loader
|---> creates thread running tenant code
     |---> creates EmbeddedSolrServer (bootstrap cl)
         |---> creates threadpool for searcherExecutor
              |---> creates threads (This inherits parent thread's security and our CL default protection domain

Итак, теперь загрузочный CL имеет объект потока, который имеет ссылку на домен защиты, который ссылается на мой загрузчик классов. Чтобы преодолеть эту проблему, нам пришлось заставить наш загрузчик классов создать домен защиты по умолчанию, который не имеет ссылки на загрузчик классов, как показано ниже, и, следовательно, освободить загрузчик классов:

    Permissions perms = new Permissions();
    perms.add(new AllPermission());
    ProtectionDomain domain = new ProtectionDomain(new CodeSource(url, new CodeSigner[0]), perms);
    defineClass(className, classBytes, 0, classBytes.length, domain);

Статические переменные

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

Темы и Пулы

Подходя к последним, но не по значимости из всех проблем. Проблемы, связанные с темой. Некоторые из стандартных моментов, которые следует помнить здесь:

  • Удалите все ThreadLocals после использования и освободите их
  • Выключите все ThreadPools, чтобы освободить потоки
  • Если потоки загружаются загрузчиком классов Bootstrap и вы вызвали setContextClassloader, удалите его.

Одна из нестандартных проблем, с которыми мы здесь столкнулись, была связана с subclassAudits. Опять же, что-то, чего мы не знали, существовало в потоках, но присутствует и может препятствовать загрузке загрузчика классов. SubclassAudit — это статическая переменная в классе Thread.java (я до сих пор не знаю, почему она там есть), и содержит ссылку на классы, производные от класса потока. Под этим я подразумеваю, что если U объявил, как мы делаем класс, производный от Thread и использующий его для запуска потоков, а не стандартного класса потока, то ссылка на этот класс сохраняется в переменной subclassAudits и остается там до бесконечности. Мы должны были вручную очистить эту переменную, используя отражение, чтобы освободить наши классы и, следовательно, загрузчик классов.

Class cls = Thread.class;
Field fld = cls.getDeclaredField("subclassAudits");
fld.setAccessible(true);
Object val = fld.get(null);
if (val != null)
{
    synchronized(val)
    {
        Map m = (Map)val;
        m.clear();
    }
}

Как только это было сделано, работа прошла гладко, загрузчики классов были освобождены, и никаких ошибок OutOfMemory не было. Выпуск загрузчика классов для сбора мусора может быть трудной работой, но это может быть сделано, даже если это только один шаг за раз и очень много времени потреблять, чтобы найти и исправить.