Java утечки памяти примеры

Диагностика утечек памяти в Java

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

Инструменты

Для успешной диагностики нам понадобятся два инструмента: Java Mission Control (jmc) и Eclipse Memory Analyzer. Вобщем-то можно обойтись только Memory Analyzer, но с JMC картина будет более полной.

Анализ использования памяти

Прежде всего, нужно запустить приложение со следующими флагами JVM:
-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder

Не используйте эти опции на production системе без приобретения специальной лицензии Oracle!

Эти опции позволят запустить Flight Recorder – утилита, которая поможет собрать информацию об использовании памяти (и много другой важной информации) во время выполнения программы. Я не буду описывать здесь как запустить Flight Recorder, эта информация легко гуглится. В моем случае было достаточно запустить FR на 10-11 минут.

Рассмотрим следующий рисунок, на котором показана классическая «пила» памяти, а так же важный сигнал, что что-то не так с использованием памяти:

Запись Fight recorder

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

Читайте также:  Стандартное нормальное распределение питон

Выявление утечки

Следующим шагом нужно выявить, что именно не доступно для очистки и в этом нам поможет Memory Analyzer. Прежде всего, нужно загрузить в программу heap dump работающего приложения с предполагаемой утечкой памяти. Это можно сделать с помощью «File → Acquire Heap Dump». После загрузки в диалоге «Getting Started Wizard» выбрать «Leak Suspects Report» после этого откроется краткий обзор возможных утечек памяти:

Leak suspects report

Если вернуться на вкладку «Overview» и выбрать «Dominator Tree», то можно увидеть более подробную картину:

Overview

Denominator tree

Дерево показывает структуру «тяжелого» объекта, а так же размер его полей (по типу). Можно видеть, что одно из полей объекта MasterTenant занимает более 45% памяти.

Устранение утечки

Имея результат анализа из предыдущего пункта, следующим шагом идет устранение накапливания объектом памяти. Тут все сильно зависит от конкретного кода. Общая рекоменация – нужно найти и проанализировать все места, где происходит инициализация или изменение соответствующего поля или полей, чтобы понять механизм накапливания памяти. В моем случае в коллекцию постоянно добавлялись записи из множества (около 150) потоков при определенных условиях.

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

Источник

Типичные случаи утечки памяти в Java

Большинству разработчиков известно, что сборщик мусора в Java не является универсальным механизмом, позволяющим программисту полностью забыть о правилах использования памяти и о том, в каких случаях осуществляется его работа. Ниже описаны типичные случаи утечки памяти в java-приложениях, встречающиеся повсеместно.
Итак, о чём должен помнить каждый java-программист.

Типичная ситуация утечки памяти:

image

Сборщик мусора периодически собирает неиспользуемые объекты, но мы видим, что график использования кучи уверенно и верно ползёт вверх.

Чем может быть вызвана данная ситуация?

Строковые операции

Наиболее частая ситуация, когда возникают утечки памяти в Java-приложениях.
Для понимания. из-за чего происходят проблемы при работе со строками, вспомним, что в Java при выполеннии таких операций, как вызов метода substring() у строки, возвращается экземпляр String лишь и изменёнными значениями переменных length и offset — длины и смещения char-последовательности. При этом, если мы получаем строку длиной 5000 символов и хотим лишь получить её префикс, используя метод substring(), то 5000 символов будут продолжать храниться в памяти.
Для систем, которые получают и обрабатывают множество сообщений, это может быть серьёзной проблемой.
Для того, чтобы избежать данную проблему, можно использовать два варианта:

String prefix = new String(longString.substring(0,5)); //первый вариант
String prefix = longString.substring(0,5).intern(); //второй вариант

Важное замечание ко второму варианту с intern-строками от Zorkus: интернированные строки хранятся не в heapspace, а в permgen space. Сборка мусора в нем происходит по отдельным правилам, не так как в heap-e / young/tenured memory pools.

Аналогично надо помнить, что схожая проблема возникает при использовании метода split().

ObjectInputStream и ObjectOutputStream

Классы ObjectInputStream и ObjectOutputStream хранят ссылки на все объекты, с которыми они работали, чтобы передавать их вместо копий. Это вызывает утечку памяти при непрерывнои использовании (к примеру, при сетевом взаимодействии).
Для решения этой проблемы необходимо периодически вызывать метод reset().

Потоки и их стек

Каждый экземпляр класса Thread в Java выделяет память для своего стека (по умолчанию, это 512 Кб; изменяется с помощью параметра -Xss). Неоптимизированные приложения, использующие множество потоков, могут привести к необоснованно высокому потреблению памяти.

Нестатичные внутренние классы

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

Паттерн Observer и связанные с ним угрозы

Часто ситуация с утечкой памяти возникает при использовании паттерна Observer (Наблюдатель). Как известно, Observer хранит список своих слушателей, которые подписаны на оповещения об определенных действиях. При этом, если мы больше не используем некий класс, который является подписчиком этого наблюдателя, то GC не сможет “собрать” его, поскольку ссылка на него хранится в самом экземпляре Observer.

Singleton

Как только экземпляр-синглтон был инициализирован, он остаётся в памяти на всё время жизни приложения. Как следствие, данный экземпляр не сможет быть собран сборщиком. Данный паттерн стоит применять лишь тогда, когда это обосновано реальными требованию к постоянному хранению в памяти.

ThreadLocal-переменные

Ссылка на ThreadLocal-переменную используется связанным с ней потоком. В большинстве серверов приложений потоки переиспользуются в пулах, следовательно ThreadLocal-данные не будут собраны GC. Если приложение само не заботится об очищении этих значений, это повлечёт серьёзную утечку памяти.

Изменяемые статичные объекты

Также частым случаям утечки памяти в Java-приложениях служит неправильное использование static. Статичная переменная хранится своим классом, а как следствие, его загрузчиком (classloader). По причине внешнего использования увеличивается шанс, что сборщик мусора не соберёт данный экземпляр. Также зачастую в static-переменных кэшируется информация или же хранятся состояния, используемые несколькими потоками. Отдельным примером являются статичные коллекции. Хорошим же тоном при архитектурном проектировании служит полное избегание изменяемых статичных объектов — зачастую существует лучшая альтернатива.

Создание объектов

Рассмотрим два случая.
Случай 1:

Elem e;
e = new HeavyElem();
e = new HeavyElem();

Elem e;
e = new HeavyElem();
e = null;
e = new HeavyElem();

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

Загрузчики классов

Ссылки на классы используются их загрузчиками и обычно не собираются GC до того, пока сам classloader не будет собран. Это часто возникат, например, в ситуациях с выгрузкой приложений из OSGi контейнера. Следует также помнить об этом и предпринимать соответствующие меры.

Как же их избежать?

А теперь для закрепления несколько советов, как избежать проблемы с утечками памяти:
1. Используйте профайлеры. Профайлер помогает увидеть, какие объекты располагаются в куче (а также просмотреть их прямо по экземплярам), что позволит на ранних стадиях отловить утечки
2. Осторожнее используйте строковые операции, особенно в случаях, когда программа работает над обработкой множества текстовых данных.
3. Всегда внимательно следите, нужны ли вам нестатичные внутренние классы, статичные переменные
4. Очищайте коллекции объектов после того, как данные были обработаны и не нуждаются в дальнейшем использовании

Пишите хороший код и не забывайте о правилах обращения с памятью!

Источник

Оцените статью