☕ Разбираемся, почему в Java утекает память несмотря на сборщик мусора
Сборщик мусора облегчает написание кода и справляется с основными проблемами, но не гарантирует полного отсутствия утечек памяти. Изучите базовые принципы его работы, чтобы понять, какими видами мусора он заниматься не будет.
java.lang.OutOfMemoryError
, которая завершит работу программы.Как сборщик мусора ищет мусор?
Как же сборщик мусора (англ. Garbage Collector, GC) определяет ненужность объекта? Базовый принцип работы GC – это поиск объектов, на которые программа потеряла ссылки.
Рассмотрим пример вызова метода, внутри которого создадим объект, используем его для подсчёта примитивного значения и без сохранения куда-либо созданного объекта возвращаем посчитанное значение:
После завершения вызова метода isGreetingTooHard
локальная переменная msg
будет уничтожена и в нашей программе не останется ни одной ссылки на созданный объект строки. Значит, программа физически не сможет использовать объект нигде после вызова метода и GC может спокойно удалить его из памяти, не опасаясь что обращения нему в будущем.
В программах часто объекты путешествуют из одних методов в другие, сохраняются и удаляются из полей, массивов, коллекций, что усложняет определение факта потери на них ссылок. Но для GC это не является неразрешимой проблемой и он способен определить, можно ли до объекта дойти по ссылкам из существующей в этот момент ячейки программы (переменной, параметра,..), и, если нельзя, то этот объект точно является мусором, ведь программа его физически уже никогда не сможет использовать.
Когда мусор не найдётся?
Объект может быть достижим из живых локальных переменных / статических полей и других активных ячеек программы, но при этом всё равно быть уже ненужным. Этот объект будет мусором, но GC не будет очищать память от него. Рассмотрим некоторые примеры подобных и других утечек памяти в Java.
Статические поля
В отличие от нестатических полей, которые существуют пока не удалён содержащий их объект, статические поля обычно живут вплоть до завершения программы. Если в статическом поле сохранена ссылка на объект, то этот объект всегда будет считаться достижимым из программы и GC удалять его не будет. Если этот объект больше не нужен, то поле следует об-null-ить самому, чтобы GC смог его очистить.
Незакрытые ресурсы
Под открытые ресурсы (например, соединения или потоки ввода-вывода) выделяется память, которая освобождается при закрытии через вызов метода close
. Уже ненужные программе но незакрытые ресурсы могут блокировать освобождение занятой памяти. Частая причина этого вида утечки это ошибка в программе, из-за которой ресурс не закрывается, но при этом ссылка на объект ресурса теряется. Например, в следующем примере сканнер закрыт не будет, если из входного потока будет считан 0, на который попытаются разделить:
Чтобы избежать этого рода утечки памяти, используйте try-with-resources
для автозакрытия ресурсов даже в случае возникновения исключений (до Java 7 используйте для этих целей finally
):
Внутренние классы
Ряд механизмов в Java предполагают наличие неявной ссылки на объект. К числу таких относится и внутренний класс. Рассмотрим пример:
Класс описывает объект с информацией о стране – в полях содержатся имя главы государства (поле headOfState
) и имена всех жителей (большой массив population
). Внутренний класс описывает объект министра иностранных дел – поле количества успешных договоров и метод приглашения главы государства на саммит.
Теперь представим себе метод, который создаёт (и никуда не сохраняет) объект страны, спрашивает у него объект министра иностранных дел и возвращает только министра из метода:
Будет ошибкой заключить, что созданный объект страны будет подчищен GC после завершения метода. Класс министра это внутренний классом, его объекту доступны все поля объекта страны, от которой он создан. Для этого джава будет поддерживать неявную ссылку в объекте министра на объект страны, храня в памяти его вместе со всеми полями, включая огромный массив с именами жителей, который нашей программе логически не нужен, а значит является мусором.
Чтобы избежать подобной утечки памяти, достаточно помнить об особенностях работы внутренних классов. Если критично, можно всегда использовать вместо них статические вложенные классы, у которых такого эффекта нет.
Собственные структуры данных
Утечка памяти это частая ошибка при проектировании своих собственных структур данных. Представим себе, что мы решили написать собственную реализацию стека, т.е. набор данных с двумя операциями: push(значение)
вставляет новое значение в конец набора; pop()
вынимает значение с конца набора.
Рассмотрим упрощённую реализацию без красивой обработки ошибок:
При создании указывается максимальный размер стека и заводится массив, в котором будут храниться вставляемые элементы. Так как длину массива менять нельзя, сразу делаем массив размером с полный стек, чтобы у были ячейки про запас, а поле nextIndex
будет указывать на первую свободную ячейку для хранения нового элемента. Схожие идеи используются во многих структурах данных, например, в ArrayList
и ArrayDeque
.
Пользоваться стеком можно так:
Наша реализация приводит к утечке памяти. И речь не о наличии ячеек в массиве про запас, проблема тут гораздо серьёзнее. Если, как в нашем примере выше, пользователь положил объект "Katya" на стек, а затем вынул и никуда не сохранил, то он ожидает удаление этого объекта из памяти через GC. Но этого не произойдёт, тк в нашем массиве ссылка на этот объект продолжит храниться, предотвращая удаление мусора из кучи.
Избавиться от этого поможет об-null-ение ячеек, значения которых больше не нужны:
Итог
Наличие встроенных алгоритмов сборки мусора ещё не гарантирует что весь мусор будет вычищаться, а занятая им память освобождаться для переиспользования. Понимание принципов работы GC помогает избежать накопления такого мусора в программе, который не будет убран автоматически.