Большинству разработчиков известно, что сборщик мусора в Java не является универсальным механизмом, позволяющим программисту полностью забыть о правилах использования памяти и о том, в каких случаях осуществляется его работа. Ниже описаны типичные случаи утечки памяти в java-приложениях, встречающиеся повсеместно.
Итак, о чём должен помнить каждый java-программист.
Типичная ситуация утечки памяти:
Сборщик мусора периодически собирает неиспользуемые объекты, но мы видим, что график использования кучи уверенно и верно ползёт вверх.
Чем может быть вызвана данная ситуация?
Наиболее частая ситуация, когда возникают утечки памяти в Java-приложениях.
Для понимания. из-за чего происходят проблемы при работе со строками, вспомним, что в Java при выполеннии таких операций, как вызов метода substring() у строки, возвращается экземпляр String лишь и изменёнными значениями переменных length и offset — длины и смещения char-последовательности. При этом, если мы получаем строку длиной 5000 символов и хотим лишь получить её префикс, используя метод substring(), то 5000 символов будут продолжать храниться в памяти.
Для систем, которые получают и обрабатывают множество сообщений, это может быть серьёзной проблемой.
Для того, чтобы избежать данную проблему, можно использовать два варианта:
Важное замечание ко второму варианту с intern-строками от Zorkus: интернированные строки хранятся не в heapspace, а в permgen space. Сборка мусора в нем происходит по отдельным правилам, не так как в heap-e / young/tenured memory pools.
Аналогично надо помнить, что схожая проблема возникает при использовании метода split().
Классы ObjectInputStream и ObjectOutputStream хранят ссылки на все объекты, с которыми они работали, чтобы передавать их вместо копий. Это вызывает утечку памяти при непрерывнои использовании (к примеру, при сетевом взаимодействии).
Для решения этой проблемы необходимо периодически вызывать метод reset().
Каждый экземпляр класса Thread в Java выделяет память для своего стека (по умолчанию, это 512 Кб; изменяется с помощью параметра -Xss). Неоптимизированные приложения, использующие множество потоков, могут привести к необоснованно высокому потреблению памяти.
Каждый нестатичный внутренний класс, который вы используете, хранит ссылку на внешний класс. Это приводит к хранению большого графа объектов, что негативно сказывается на использовании памяти. В ситуациях, где явно не нужно использовать ссылку на внешний класс, реализовывайте внутренние классы статичными.
Часто ситуация с утечкой памяти возникает при использовании паттерна Observer (Наблюдатель). Как известно, Observer хранит список своих слушателей, которые подписаны на оповещения об определенных действиях. При этом, если мы больше не используем некий класс, который является подписчиком этого наблюдателя, то GC не сможет “собрать” его, поскольку ссылка на него хранится в самом экземпляре Observer.
Как только экземпляр-синглтон был инициализирован, он остаётся в памяти на всё время жизни приложения. Как следствие, данный экземпляр не сможет быть собран сборщиком. Данный паттерн стоит применять лишь тогда, когда это обосновано реальными требованию к постоянному хранению в памяти.
Ссылка на ThreadLocal-переменную используется связанным с ней потоком. В большинстве серверов приложений потоки переиспользуются в пулах, следовательно ThreadLocal-данные не будут собраны GC. Если приложение само не заботится об очищении этих значений, это повлечёт серьёзную утечку памяти.
Также частым случаям утечки памяти в Java-приложениях служит неправильное использование static. Статичная переменная хранится своим классом, а как следствие, его загрузчиком (classloader). По причине внешнего использования увеличивается шанс, что сборщик мусора не соберёт данный экземпляр. Также зачастую в static-переменных кэшируется информация или же хранятся состояния, используемые несколькими потоками. Отдельным примером являются статичные коллекции. Хорошим же тоном при архитектурном проектировании служит полное избегание изменяемых статичных объектов — зачастую существует лучшая альтернатива.
Рассмотрим два случая.
Случай 1:
Случай 2:
Второй случай является более правильным, поскольку в большинстве случаев это явно укажет сборщику на сбор неиспользванного экземпляра первоначально созданного HeavyElem.
Ссылки на классы используются их загрузчиками и обычно не собираются GC до того, пока сам classloader не будет собран. Это часто возникат, например, в ситуациях с выгрузкой приложений из OSGi контейнера. Следует также помнить об этом и предпринимать соответствующие меры.
А теперь для закрепления несколько советов, как избежать проблемы с утечками памяти:
1. Используйте профайлеры. Профайлер помогает увидеть, какие объекты располагаются в куче (а также просмотреть их прямо по экземплярам), что позволит на ранних стадиях отловить утечки
2. Осторожнее используйте строковые операции, особенно в случаях, когда программа работает над обработкой множества текстовых данных.
3. Всегда внимательно следите, нужны ли вам нестатичные внутренние классы, статичные переменные
4. Очищайте коллекции объектов после того, как данные были обработаны и не нуждаются в дальнейшем использовании
Пишите хороший код и не забывайте о правилах обращения с памятью!
Итак, о чём должен помнить каждый java-программист.
Типичная ситуация утечки памяти:
Сборщик мусора периодически собирает неиспользуемые объекты, но мы видим, что график использования кучи уверенно и верно ползёт вверх.
Чем может быть вызвана данная ситуация?
Строковые операции
Наиболее частая ситуация, когда возникают утечки памяти в 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();
Случай 2:
Elem e;
e = new HeavyElem();
e = null;
e = new HeavyElem();
Второй случай является более правильным, поскольку в большинстве случаев это явно укажет сборщику на сбор неиспользванного экземпляра первоначально созданного HeavyElem.
Загрузчики классов
Ссылки на классы используются их загрузчиками и обычно не собираются GC до того, пока сам classloader не будет собран. Это часто возникат, например, в ситуациях с выгрузкой приложений из OSGi контейнера. Следует также помнить об этом и предпринимать соответствующие меры.
Как же их избежать?
А теперь для закрепления несколько советов, как избежать проблемы с утечками памяти:
1. Используйте профайлеры. Профайлер помогает увидеть, какие объекты располагаются в куче (а также просмотреть их прямо по экземплярам), что позволит на ранних стадиях отловить утечки
2. Осторожнее используйте строковые операции, особенно в случаях, когда программа работает над обработкой множества текстовых данных.
3. Всегда внимательно следите, нужны ли вам нестатичные внутренние классы, статичные переменные
4. Очищайте коллекции объектов после того, как данные были обработаны и не нуждаются в дальнейшем использовании
Пишите хороший код и не забывайте о правилах обращения с памятью!