Статьи

OpenJPA: пример утечки памяти

В этой статье представлены подробные сведения об анализе первопричин и устранении утечки памяти кучи Java (утечки Apache OpenJPA), влияющей на производственную среду Oracle Weblogic server 10.0. В этом посте также будет продемонстрировано, что при управлении жизненным циклом javax.persistence.EntityManagerFactory важно следовать рекомендациям API персистентности Java.

Спецификации окружающей среды

  • Сервер Java EE : Oracle Weblogic Portal 10.0
  • ОС : солярис 10
  • JDK : Oracle / Sun HotSpot JVM 1.5, 32-разрядная, емкость 2 ГБ
  • API персистентности Java : Apache OpenJPA 1.0.x (спецификации JPA 1.0)
  • СУБД : Oracle 10g
  • Тип платформы : веб-портал

Инструменты для устранения неполадок

Описание проблемы и наблюдения

Первоначально о проблеме сообщала наша служба поддержки производства Weblogic после перебоев в работе. Первоначальный анализ основных причин выявил следующие факты и наблюдения:

  • Отключения производства наблюдались на регулярной основе после ~ 2 недель движения.
  • Сбои были вызваны истощением кучи Java (OldGen), например, OutOfMemoryError: ошибка пространства кучи Java, обнаруженная в журналах Weblogic.
  • Утечка памяти кучи Java была подтверждена после проверки использования пространства OldGen кучи Java с течением времени из инструмента мониторинга Foglight вместе с подробными историческими данными Java GC .

После обнаружения вышеуказанных проблем было принято решение перейти к следующему этапу RCA и выполнить анализ дампа кучи JVM для затронутых экземпляров Weblogic (JVM).

Анализ дампа кучи JVM

** Видео, объясняющее следующий анализ дампа кучи JVM, теперь доступно здесь . Для того, чтобы создать
Дамп кучи JVM поддерживаемая группа использовала утилиту HotSpot 1.5 jmap, которая генерировала файл дампа кучи (heap.bin) размером ~ 1,5 ГБ. Затем файл дампа кучи был проанализирован с помощью Eclipse Memory Analyzer Tool . Теперь давайте рассмотрим анализ дампа кучи, чтобы понять причину утечки памяти в OldGen.

MAT предоставляет первоначальный отчет Leak Suspect, который может быть очень полезен для выделения ваших участников с большим объемом памяти. В нашем проблемном случае MAT удалось выявить подозреваемого на наличие утечки, что составляет почти 600 МБ или 40% от общей емкости OldGen.

В этот момент мы нашли один экземпляр java.util.LinkedList, использующий почти 600 МБ памяти, и загрузили его в один из загрузчиков родительских классов нашего приложения (@ 0x7e12b708). Следующим шагом было понимание протекающих объектов вместе с источником удержания. MAT позволяет вам проверять любой экземпляр загрузчика классов вашего приложения, предоставляя вам возможность проверять загруженные классы и экземпляры. Просто найдите нужный объект, указав адрес, например, 0x7e12b708, а затем просмотрите загруженные классы и экземпляры, выбрав Список объектов> с исходящими ссылками.

Как видно из приведенного выше снимка, анализ оказался довольно показательным. То, что мы обнаружили, было одним экземпляром org.apache.openjpa.enhance.PCRegistry в источнике сохранения памяти; точнее виновником было поле _listeners, реализованное как LinkedList. Для справки, Apache OpenJPA PCRegistry используется для отслеживания зарегистрированных классов с поддержкой персистентности. Ниже приведен фрагмент исходного кода PCRegistry из Apache OpenJPA версии 1.0.4, раскрывающий поле _listeners .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
/**
 * Tracks registered persistence-capable classes.
 *
 * @since 0.4.0
 * @author Abe White
 */
public class PCRegistry {
    // DO NOT ADD ADDITIONAL DEPENDENCIES TO THIS CLASS
 
    private static final Localizer _loc = Localizer.forPackage
        (PCRegistry.class);
 
    // map of pc classes to meta structs; weak so the VM can GC classes
    private static final Map _metas = new ConcurrentReferenceHashMap
        (ReferenceMap.WEAK, ReferenceMap.HARD);
 
    // register class listeners
    private static final Collection _listeners = new LinkedList();

Теперь вопрос в том, почему объем памяти этой внутренней структуры данных настолько велик и потенциально может течь со временем? Следующим шагом было глубокое погружение в экземпляр _listeners LinkedLink, чтобы проанализировать протекающие объекты.

Наконец, мы обнаружили, что объекты утечки на самом деле были определениями отображения JDBC и SQL (метаданными), используемыми нашим приложением для выполнения различных запросов к нашей базе данных Oracle. Обзор спецификаций JPA, документации OpenJPA и источника подтвердил, что основная причина была связана с неправильным использованием javax.persistence.EntityManagerFactory, например отсутствием закрытия вновь созданного экземпляра EntityManagerFactory.

Если вы внимательно посмотрите на приведенный выше снимок кода, вы поймете, что метод close () действительно отвечает за очистку любого недавно использованного экземпляра хранилища метаданных. Это также вызвало еще одну проблему: почему мы создаем такие экземпляры Factory снова и снова … Следующим шагом исследования было выполнение кода нашего приложения, особенно в отношении управления жизненным циклом объектов JPA EntityManagerFactory и EntityManager.

Основная причина и решение

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class Application {
       
       @Resource
       private UserTransaction utx = null;
        
       // Initialized on each application request and not closed!
       @PersistenceUnit(unitName = "UnitName")
       private EntityManagerFactory emf = Persistence.createEntityManagerFactory("PersistenceUnit");
 
       public EntityManager getEntityManager() {
             return this.emf.createEntityManager();
       }
       
       public void businessMethod() {
             
             // Create a new EntityManager instance via from the newly created EntityManagerFactory instance
             // Do something...
             // Close the EntityManager instance
       }
}

Этот дефект кода и ненадлежащее использование JPA EntityManagerFactory вызывали утечку или накопление экземпляров репозитория метаданных в структуре данных OpenJPA _listeners, продемонстрированной в более раннем анализе дампа кучи JVM. Решением проблемы было централизовать управление и жизненный цикл потокобезопасного javax.persistence.EntityManagerFactory с помощью шаблона Singleton. Окончательное решение было реализовано в соответствии с ниже:

  • Создайте и поддерживайте только один статический экземпляр javax.persistence.EntityManagerFactory для каждого загрузчика классов приложений и реализуйте его с помощью шаблона Singleton.
  • Создайте и утилизируйте новые экземпляры EntityManager для каждого запроса приложения.

Пожалуйста, просмотрите это обсуждение от Stackoverflow, так как решение, которое мы внедрили, очень похоже. После внедрения решения в нашу производственную среду утечки памяти в старой куче Java больше не наблюдается.

Ссылка: OpenJPA: пример утечки памяти от нашего партнера JCG Пьера-Хьюга Шарбонно из блога поддержки шаблонов Java EE и учебного курса по Java .