☕ Разбираемся, почему в Java утекает память несмотря на сборщик мусора
Telegram: @fivoronov Сборщик мусора облегчает написание кода и справляется с основными проблемами, но не гарантирует полного отсутствия утечек памяти. Изучите базовые принципы его работы, чтобы понять, какими видами мусора он заниматься не будет. Что такое утечка памяти? Утечкой называют присутствие в памяти ненужных программе объектов и данных. На их содержание тратятся ресурсы и в конечном итоге это может привести к ошибке работы виртуальной машины Как же сборщик мусора (англ. Garbage Collector, GC) определяет ненужность объекта? Базовый принцип работы GC – это поиск объектов, на которые программа потеряла ссылки. Рассмотрим пример вызова метода, внутри которого создадим объект, используем его для подсчёта примитивного значения и без сохранения куда-либо созданного объекта возвращаем посчитанное значение: После завершения вызова метода В программах часто объекты путешествуют из одних методов в другие, сохраняются и удаляются из полей, массивов, коллекций, что усложняет определение факта потери на них ссылок. Но для GC это не является неразрешимой проблемой и он способен определить, можно ли до объекта дойти по ссылкам из существующей в этот момент ячейки программы (переменной, параметра,..), и, если нельзя, то этот объект точно является мусором, ведь программа его физически уже никогда не сможет использовать. Поиск мусора через достижимость объектов Объект может быть достижим из живых локальных переменных / статических полей и других активных ячеек программы, но при этом всё равно быть уже ненужным. Этот объект будет мусором, но GC не будет очищать память от него. Рассмотрим некоторые примеры подобных и других утечек памяти в Java. Больше полезной информации вы найдете на нашем телеграм-канале «Библиотека джависта». В отличие от нестатических полей, которые существуют пока не удалён содержащий их объект, статические поля обычно живут вплоть до завершения программы. Если в статическом поле сохранена ссылка на объект, то этот объект всегда будет считаться достижимым из программы и GC удалять его не будет. Если этот объект больше не нужен, то поле следует об-null-ить самому, чтобы GC смог его очистить. Под открытые ресурсы (например, соединения или потоки ввода-вывода) выделяется память, которая освобождается при закрытии через вызов метода Чтобы избежать этого рода утечки памяти, используйте Ряд механизмов в Java предполагают наличие неявной ссылки на объект. К числу таких относится и внутренний класс. Рассмотрим пример: Country.java Класс описывает объект с информацией о стране – в полях содержатся имя главы государства (поле Теперь представим себе метод, который создаёт (и никуда не сохраняет) объект страны, спрашивает у него объект министра иностранных дел и возвращает только министра из метода: Будет ошибкой заключить, что созданный объект страны будет подчищен GC после завершения метода. Класс министра это внутренний классом, его объекту доступны все поля объекта страны, от которой он создан. Для этого джава будет поддерживать неявную ссылку в объекте министра на объект страны, храня в памяти его вместе со всеми полями, включая огромный массив с именами жителей, который нашей программе логически не нужен, а значит является мусором. Чтобы избежать подобной утечки памяти, достаточно помнить об особенностях работы внутренних классов. Если критично, можно всегда использовать вместо них статические вложенные классы, у которых такого эффекта нет. Утечка памяти это частая ошибка при проектировании своих собственных структур данных. Представим себе, что мы решили написать собственную реализацию стека, т.е. набор данных с двумя операциями: Рассмотрим упрощённую реализацию без красивой обработки ошибок: LeakyStack.java При создании указывается максимальный размер стека и заводится массив, в котором будут храниться вставляемые элементы. Так как длину массива менять нельзя, сразу делаем массив размером с полный стек, чтобы у были ячейки про запас, а поле Пользоваться стеком можно так: Наша реализация приводит к утечке памяти. И речь не о наличии ячеек в массиве про запас, проблема тут гораздо серьёзнее. Если, как в нашем примере выше, пользователь положил объект “Katya” на стек, а затем вынул и никуда не сохранил, то он ожидает удаление этого объекта из памяти через GC. Но этого не произойдёт, тк в нашем массиве ссылка на этот объект продолжит храниться, предотвращая удаление мусора из кучи. Избавиться от этого поможет об-null-ение ячеек, значения которых больше не нужны: Наличие встроенных алгоритмов сборки мусора ещё не гарантирует что весь мусор будет вычищаться, а занятая им память освобождаться для переиспользования. Понимание принципов работы GC помогает избежать накопления такого мусора в программе, который не будет убран автоматически. Filipp Voronov
java.lang.OutOfMemoryError
, которая завершит работу программы. Как сборщик мусора ищет мусор?
public static boolean isGreetingTooHard() { String msg = "Hello World!"; return msg.length() > 10; }
isGreetingTooHard
локальная переменная msg
будет уничтожена и в нашей программе не останется ни одной ссылки на созданный объект строки. Значит, программа физически не сможет использовать объект нигде после вызова метода и 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; } }
Внутренние классы
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() { Country proglibLand = new Country(); return proglibLand.newForeignMinister(); }
Собственные структуры данных
push(значение)
вставляет новое значение в конец набора; pop()
вынимает значение с конца набора.
public class LeakyStack<T> { 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<String> stack = new LeakyStack<>(10); stack.push("Petya"); stack.push("Katya"); System.out.println(stack.pop()); // Katya
public T pop() { T popped = data[--nextIndex]; data[nextIndex] = null; return popped; }
Итог
- 0 views
- 0 Comment
Свежие комментарии