Неделю назад меня попросили исправить проблемное веб-приложение, страдающее от утечек памяти . Как это может быть трудно, подумал я, учитывая, что за последние два года я видел и исправлял сотни утечек.
Но этот оказался проблемой. 12 часов спустя я обнаружил не менее пяти утечек в приложении и сумел устранить четыре из них. Я подумал, что это будет опыт, которым стоит поделиться. Для нетерпеливых — в общем, я обнаружил утечки из
- Драйверы MySQL запускают фоновые потоки
- java.sql.DriverManager не выгружается при повторном развертывании
- BoneCP загружает ресурсы из неправильных загрузчиков классов
- Источник данных, зарегистрированный в дереве JNDI, блокирующий выгрузку
- Пул соединений с использованием финализаторов связан с реализацией справочной очереди Google в отдельном потоке
Под этим приложением находилось простое веб-приложение на Java с несколькими источниками данных, подключенными к реляционным базам данных, в середине Spring для склеивания элементов и простые страницы JSP, отображаемые для конечного пользователя. Никакой магии. Или я так думал. Мальчик, я был неправ.
Первая остановка — драйверы MySQL . Очевидно, что наиболее распространенные драйверы MySQL запускают поток в фоновом режиме, очищая ваши неиспользуемые и незакрытые соединения. Все идет нормально. Но суть в том, что загрузчик классов контекста этого нового потока является загрузчиком классов вашего веб-приложения. Это означает, что пока этот поток запущен, а вы пытаетесь удалить свое веб-приложение, его загрузчик классов остается позади — со всеми загруженными в него классами.
Очевидно, что с июля 2012 года по февраль 2013 года это было исправлено после обнаружения ошибки. Вы можете следить за обсуждением в трекере проблем MySQL . Наконец, реализованным решением был метод shutdown () для API, который вы, как разработчик, должны знать, чтобы вызывать его перед повторным развертыванием. Ну, я не сделал. Бьюсь об заклад, 99% из вас там тоже не сделали.
В вашем типичном веб-приложении Java есть хорошее место для таких перехватчиков завершения работы, а именно метод contextDestroyed () класса ServletContextListener . Этот конкретный метод вызывается каждый раз, когда контекст сервлета уничтожается, что чаще всего происходит, например, при повторном развертывании. Скорее всего, довольно немногие разработчики знают, что это место существует, но сколько на самом деле осознают необходимость очистки в этом конкретном хуке?
Вернуться к приложению, которое было еще далеко от исправления. Мое второе открытие также было связано с контекстными загрузчиками классов и источниками данных. Когда вы используете com.jdbc.myslq.Driver, он регистрируется как драйвер в классе java.sql.DriverManager . Опять же, это сделано с добрыми намерениями. В конце концов, это то, что ваше приложение использует для определения правильного выбора драйвера для каждого запроса при подключении к URL-адресу базы данных. Но, как вы можете догадаться, есть одна загвоздка: этот DriverManager загружается в загрузчик классов загрузчика, а не в загрузчик классов вашего веб-приложения, поэтому не может быть выгружен при повторном развертывании приложения.
Что сейчас делает вещи действительно странными, так это то, что не существует общего способа отменить регистрацию водителя самостоятельно. Ссылка на класс, который вы пытаетесь отменить, кажется, намеренно скрыта от вас. В этом конкретном случае мне повезло, и пул соединений, использованный в приложении, смог отменить регистрацию драйвера. На случай, если я не забуду спросить. Оглядываясь назад на подобные случаи в моем прошлом, я впервые увидел такую функцию, реализованную в пуле соединений. До этого мне приходилось перечислять все драйверы JDBC, зарегистрированные в DriverManager, чтобы выяснить, какие из них следует отменить . Не опыт, который я могу рекомендовать никому.
Так и должно быть, подумал я. Двух утечек в одном приложении уже больше, чем можно терпеть. Неправильно. Третья проблема, которая возникла прямо у меня из отчета об утечке, была sun.awt.AppContext со статическим полем mainAppContext. Какая? Я понятия не имею, что должен делать этот класс, но я был почти уверен, что приложение под рукой не использовало AWT . Поэтому я начал отладчик, чтобы узнать, кто загружает этот класс (и почему). Еще один сюрприз: это был com.sun.jmx.trace.Trace.out (). Можете ли вы найти вескую причину, по которой класс com.sun.jmx будет называть класс sun.awt? Я конечно не могу. Тем не менее этот стек классов возник из пула соединений BoneCP . И есть абсолютно нулевой способ пропустить эту строку кода, которая приводит к этой конкретной утечке памяти. Решение? Следующее магическое заклинание в моем ServletContextListener.contextInitialized () :
1
|
Thread.currentThread().setContextClassLoader(null); // Force the AppContext singleton to be created and initialized without holding reference to WebAppClassLoder sun.awt.AppContext.getAppContext(); |
Но я все еще не сделал: что-то все еще протекало. В этом случае я обнаружил, что наше приложение связывает этот источник данных с JNDI- деревом InitialContext () , хорошим стандартизированным способом связывания ваших объектов для будущего обнаружения. Но опять же — при использовании этой приятной вещи вам пришлось убирать за собой, отсоединив этот источник данных от дерева JNDI в том же методе contextDestroy () .
Ну, пока у нас были довольно логичные, хотя и редкие и несколько неясные проблемы, но с некоторыми рассуждениями и гугл-фу были быстро исправлены. Моя пятая и последняя проблема не была такой. У меня все еще было сбой этого приложения с OutOfMemoryError: PermGen . И Plumbr, и Eclipse MAT сообщили мне, что виновником, взявшим в заложники моего загрузчика классов, была ветка с именем com.google.common.base.internal.Finalizer. «Кто, черт возьми, этот парень?» — была моя последняя мысль, прежде чем тьма поглотила меня. Через пару часов и четыре кофе я обнаружил, что смотрю на три строки:
1
2
3
|
emf.close(); emf = null; ds = null; |
Трудно вспомнить, что именно произошло за прошедшие часы. У меня есть отдаленные воспоминания о WeakReferences , ReferenceQueues , Finalizer , Reflection и моей первой встрече с PhantomReference в дикой природе. Даже сегодня я до сих пор не могу полностью объяснить, почему и для какой цели пул соединений использовал финализаторы, связанные с реализацией справочной очереди Google, работающей в отдельном потоке.
Также я не могу объяснить, почему закрытие javax.persistence.EntityManagerFactory (с именем emf в приведенном выше коде и хранится в статической ссылке в одном из собственных классов приложения) было недостаточно; и поэтому мне пришлось вручную обнулить эту ссылку. И аналогичная статическая ссылка на источник данных, используемый этой фабрикой. Я был уверен, что Java GC может справиться с циклическими ссылками в течение всего дня, но кажется, что это волшебное кольцо классов, статических ссылок, объектов, финализаторов и очередей ссылок было слишком сложным даже для него. И снова, впервые в моей долгой карьере, мне пришлось аннулировать ссылку на Java.
Я скромный парень и поэтому не могу утверждать, что я был самым эффективным в поиске лекарства от всего вышеперечисленного всего за 12 часов. Но я должен признать, что я имел дело с утечками памяти почти исключительно в течение последних трех лет. И у меня даже было свое собственное создание, Пламбр , которое помогало мне (на самом деле, четыре из пяти утечек были обнаружены Пламбром за 30 минут или около того). Но чтобы на самом деле устранить эти утечки, мне потребовалось больше, чем полный рабочий день.
В целом — что-то явно сломано в мире Java EE и / или загрузчика классов. Не может быть нормальным, что разработчик должен помнить все эти ловушки и приемы настройки, потому что это просто невозможно. В конце концов, нам нравится использовать наши головы для чего-то продуктивного. И, как видно из обходных путей, связанных с двумя популярными контейнерами сервлетов ( Tomcat и Jetty ), проблема серьезна. Однако для ее решения потребуется нечто большее, чем просто смягчение некоторых симптомов, но устранение основных ошибок проектирования.
Ссылка: Поиск утечек памяти: пример из практики нашего партнера по JCG Никиты Сальникова Тарновского в блоге Plumbr Blog .