Статьи

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 .

/ **

 * Отслеживает зарегистрированные персистентные классы.

 *

 *
@since 0.4.0

 *
@author
Abe White

 * /
открытый
класс
PCRegistry {

  // НЕ ДОБАВЛЯЙТЕ ДОПОЛНИТЕЛЬНЫХ ЗАВИСИМОСТЕЙ К ЭТОМУ КЛАССУ
 
private
static
final
Localizer
_loc =
Локализатор .forPackage

  (PCRegistry.
Класс );

  // отображение
классов
pc в
метаструктуры ; слабый, так что виртуальная машина может GC классы
 
приватная
статическая
финальная
карта
_metas =
new
ConcurrentReferenceHashMap
  (ReferenceMap.WEAK, ReferenceMap.HARD) ;

  // регистрируем слушателей класса
 
приватная
статическая
финальная
коллекция
_listeners =
new
LinkedList ();

……………………………………………………………………………………

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

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

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

Следующим этапом исследования было выполнение кода нашего приложения, особенно вокруг управления жизненным циклом объектов JPA EntityManagerFactory и EntityManager.
Основная причина и решение

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

 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 больше не наблюдается.

Пожалуйста, не стесняйтесь оставлять свои комментарии и делиться своим опытом.