Java stack memory leak

Типичные случаи утечки памяти в 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.

Читайте также:  New css border styles

Аналогично надо помнить, что схожая проблема возникает при использовании метода 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. Очищайте коллекции объектов после того, как данные были обработаны и не нуждаются в дальнейшем использовании

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

Источник

Java stack memory leak

После завершения вызова метода isGreetingTooHard локальная переменная msg будет уничтожена и в нашей программе не останется ни одной ссылки на созданный объект строки. Значит, программа физически не сможет использовать объект нигде после вызова метода и GC может спокойно удалить его из памяти, не опасаясь что обращения нему в будущем.

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

Поиск мусора через достижимость объектов

Когда мусор не найдётся?

Объект может быть достижим из живых локальных переменных / статических полей и других активных ячеек программы, но при этом всё равно быть уже ненужным. Этот объект будет мусором, но GC не будет очищать память от него. Рассмотрим некоторые примеры подобных и других утечек памяти в Java.

Статические поля

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

Незакрытые ресурсы

Под открытые ресурсы (например, соединения или потоки ввода-вывода) выделяется память, которая освобождается при закрытии через вызов метода close . Уже ненужные программе но незакрытые ресурсы могут блокировать освобождение занятой памяти. Частая причина этого вида утечки это ошибка в программе, из-за которой ресурс не закрывается, но при этом ссылка на объект ресурса теряется. Например, в следующем примере сканнер закрыт не будет, если из входного потока будет считан 0, на который попытаются разделить:

public static double divide() < Scanner scanner = new Scanner(. ); double result = 1; while (scanner.hasNextInt()) < result /= scanner.nextInt(); >scanner.close(); return result; > 

Чтобы избежать этого рода утечки памяти, используйте try-with-resources для автозакрытия ресурсов даже в случае возникновения исключений (до Java 7 используйте для этих целей finally ):

public static double divide() < try (Scanner scanner = new Scanner(. )) < double result = 1; while (scanner.hasNextInt()) < result /= scanner.nextInt(); >return result; > > 

Внутренние классы

Ряд механизмов в Java предполагают наличие неявной ссылки на объект. К числу таких относится и внутренний класс. Рассмотрим пример:

public class Country < //Население страны protected String[] population = new String[1_000_000_000]; protected String headOfState = "Mr. President"; public ForeignMinister newForeignMinister() < return new ForeignMinister(); >public class ForeignMinister < protected int deals = 0; // количество успешных сделок public void acceptInvitation() < deals++; System.out.println(headOfState + " приглашён на глобальную встречу"); >> > 

Класс описывает объект с информацией о стране – в полях содержатся имя главы государства (поле headOfState ) и имена всех жителей (большой массив population ). Внутренний класс описывает объект министра иностранных дел – поле количества успешных договоров и метод приглашения главы государства на саммит.

Теперь представим себе метод, который создаёт (и никуда не сохраняет) объект страны, спрашивает у него объект министра иностранных дел и возвращает только министра из метода:

public static ForeignMinister getAnyMinister()

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

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

🧩☕ Интересные задачи по Java для практики можно найти на нашем телеграм-канале «Библиотека задач по Java»

Собственные структуры данных

Утечка памяти это частая ошибка при проектировании своих собственных структур данных. Представим себе, что мы решили написать собственную реализацию стека, т.е. набор данных с двумя операциями: push(значение) вставляет новое значение в конец набора; pop() вынимает значение с конца набора.

Рассмотрим упрощённую реализацию без красивой обработки ошибок:

public class LeakyStack  < private T[] data; // буфер private int nextIndex; // свободная ячейка буфера public LeakyStack(int size) < data = (T[]) new Object[size]; >public void push(T value) < data[nextIndex++] = value; >public T pop() < return data[--nextIndex]; >> 

При создании указывается максимальный размер стека и заводится массив, в котором будут храниться вставляемые элементы. Так как длину массива менять нельзя, сразу делаем массив размером с полный стек, чтобы у были ячейки про запас, а поле nextIndex будет указывать на первую свободную ячейку для хранения нового элемента. Схожие идеи используются во многих структурах данных, например, в ArrayList и ArrayDeque .

Пользоваться стеком можно так:

LeakyStack stack = new LeakyStack<>(10); stack.push("Petya"); stack.push("Katya"); System.out.println(stack.pop()); // Katya 

Наша реализация приводит к утечке памяти. И речь не о наличии ячеек в массиве про запас, проблема тут гораздо серьёзнее. Если, как в нашем примере выше, пользователь положил объект «Katya» на стек, а затем вынул и никуда не сохранил, то он ожидает удаление этого объекта из памяти через GC. Но этого не произойдёт, тк в нашем массиве ссылка на этот объект продолжит храниться, предотвращая удаление мусора из кучи.

Избавиться от этого поможет об-null-ение ячеек, значения которых больше не нужны:

Итог

Наличие встроенных алгоритмов сборки мусора ещё не гарантирует что весь мусор будет вычищаться, а занятая им память освобождаться для переиспользования. Понимание принципов работы GC помогает избежать накопления такого мусора в программе, который не будет убран автоматически.

Источники

Источник

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