Сборщик мусора и метод finalize
Классический учебный пример: переопределяем finalize(), обнуляем ссылку, вызываем System.gc() и ждём в консоли строчку «Чашка исчезает навсегда». Запустите этот код на современной JDK — и консоль вполне может остаться пустой. JVM не обязана выполнять запрос на сборку мусора, не обязана успеть выполнить финализаторы до выхода из main(), а на JDK 18 и выше достаточно запустить java --finalization=disabled Cup, чтобы вывода не было гарантированно. Именно поэтому finalize() и не годится для освобождения ресурсов.
1. Сборщик мусора в Java
Сборщик мусора (garbage collector, GC) в Java — это подсистема JVM, которая автоматически находит объекты, недостижимые из работающей программы, и освобождает занятую ими память. В языках вроде C или C++ память, выделенную под объект, освобождает сам разработчик. В Java этим занимается JVM: вы создаёте объект оператором new, а когда на него не остаётся ссылок, сборщик мусора рано или поздно освободит память.
Что это даёт на практике:
- нет ручного
free()/deleteи, как следствие, нет двойного освобождения памяти и висячих указателей; - память под новые объекты выделяется быстро — в области Eden это фактически сдвиг указателя;
- расплата — паузы на сборку и отсутствие контроля над моментом освобождения памяти.
Куча (heap) в HotSpot разделена на поколения: молодое (Young: Eden и два Survivor) и старое (Old / Tenured). Большинство объектов умирает молодыми, поэтому короткая сборка молодого поколения (minor GC) отрабатывает быстро; объекты, пережившие несколько сборок, перемещаются в старое поколение, которое собирается реже и дороже (major / full GC).
Важно
Сборщик мусора управляет только кучей. Метаданные классов с Java 8 живут в Metaspace (он заменил PermGen) за пределами кучи, а память, выделенная через ByteBuffer.allocateDirect() или JNI, вообще не относится к куче — её освобождение GC напрямую не контролирует.
2. Когда объект становится мусором
Объект считается мусором, если до него нельзя добраться ни по одной цепочке ссылок от так называемых корней сборки мусора (GC roots). Корнями являются:
- локальные переменные и параметры методов в стеках всех живых потоков;
- статические поля загруженных классов;
- сами живые объекты
Thread; - ссылки из нативного кода (JNI) и объекты, используемые как мониторы синхронизации.
Рассмотрим пример. В методе main() класса Cup создаётся объект Cup, на который указывает переменная cup. После выполнения строки cup = null объект всё ещё существует в памяти, но на него не указывает ни одна ссылка — он становится кандидатом на удаление. Вместе с ним недостижимым становится и объект Spoon, на который ссылалось поле cup.spoon.
public class Spoon {
} public class Cup {
Spoon spoon;
Cup(Spoon spoon) {
this.spoon = spoon;
}
public static void main(String[] args) {
Cup cup = new Cup(new Spoon());
cup = null; // объект Cup и вложенный Spoon стали недостижимы
}
} Важное следствие модели достижимости: циклические ссылки не мешают сборке мусора. Если два объекта ссылаются друг на друга, но до них не дотянуться от GC roots, они образуют «остров изоляции» и будут собраны целиком. Это принципиальное отличие от подсчёта ссылок (reference counting), где такой цикл остался бы в памяти навсегда.
public class Island {
Island friend;
public static void main(String[] args) {
Island a = new Island();
Island b = new Island();
a.friend = b; // a -> b
b.friend = a; // b -> a
a = null;
b = null; // оба объекта недостижимы и будут собраны
}
} 3. System.gc(): можно ли запустить сборку вручную
Попросить JVM выполнить сборку мусора можно двумя эквивалентными способами:
System.gc();
Runtime.getRuntime().gc(); // то же самое, System.gc() просто делегирует сюда Ключевое слово здесь — попросить. Документация System.gc() прямо говорит, что это лишь подсказка (hint) виртуальной машине. JVM может выполнить полную сборку, выполнить её позже или проигнорировать вызов совсем. Более того, приложение можно запустить с флагом -XX:+DisableExplicitGC, и все явные вызовы System.gc() превратятся в пустую операцию.
В прикладном коде System.gc() вызывать не стоит: обычно это провоцирует дорогую полную сборку (stop-the-world) и делает поведение приложения хуже, а не лучше. Допустимые сценарии — учебные примеры, микробенчмарки и замеры памяти перед снятием heap dump.
4. Метод finalize()
protected void finalize() — метод класса Object, который поток-финализатор JVM может вызвать у объекта перед освобождением его памяти. Идея была такой: если объект владеет внешним ресурсом (файл, сокет, нативный буфер), переопределяем finalize() и закрываем ресурс там, чтобы он не утёк даже при забытом close().
Добавим finalize() в классы Spoon и Cup и попробуем спровоцировать его вызов через System.gc():
public class Spoon {
@Override
protected void finalize() {
System.out.println("Ложка исчезает навсегда");
}
} public class Cup {
private Spoon spoon;
public Cup(Spoon spoon) {
this.spoon = spoon;
}
@Override
protected void finalize() {
System.out.println("Чашка исчезает навсегда");
}
public static void main(String[] args) {
Cup cup = new Cup(new Spoon());
cup = null;
System.gc();
}
} Возможный вывод:
Чашка исчезает навсегда
Ложка исчезает навсегда Слово «возможный» здесь принципиально. Строки могут поменяться местами, может появиться только одна из них или не появиться ни одной: порядок финализации объектов не определён, а JVM спокойно завершает работу, не дожидаясь опустошения очереди финализаторов. Чтобы вывод стал стабильнее, в учебных примерах после System.gc() иногда добавляют небольшую задержку, но это костыль, а не гарантия.
Формулировка для собеседования
finalize() не вызывается при выходе объекта из области видимости, не вызывается при завершении JVM и вообще не гарантирован к вызову. Единственное, что обещает спецификация: если метод всё-таки будет вызван, то не более одного раза за время жизни объекта.
5. Почему finalize() признали устаревшим
Механизм финализации накопил столько проблем, что его решили убрать из языка:
- Непредсказуемая задержка. Между моментом, когда объект стал мусором, и вызовом
finalize()могут пройти минуты. Файловые дескрипторы и соединения кончатся раньше. - Риск исчерпания памяти. Объекты с
finalize()переживают минимум одну лишнюю сборку и попадают в очередь финализации. Если финализаторы работают медленно, очередь растёт и приложение падает сOutOfMemoryError. - Проглоченные исключения. Исключение, брошенное из
finalize(), просто игнорируется: объект остаётся в повреждённом состоянии, а в логах не будет ничего. - Воскрешение объекта. В
finalize()можно записатьthisв статическое поле и вернуть объект к жизни. Второй разfinalize()у него уже не вызовут. - Многопоточность и безопасность. Финализатор работает в отдельном потоке, что порождает гонки; классическая атака finalizer attack позволяет получить доступ к недостроенному объекту, конструктор которого бросил исключение.
- Накладные расходы. Создание и сборка объектов с финализатором заметно дороже обычных.
Хронология статуса метода:
| Версия Java | Что произошло с финализацией |
|---|---|
| Java 9 | Object.finalize() помечен как @Deprecated; добавлен класс java.lang.ref.Cleaner как замена |
| Java 11 | Удалены System.runFinalizersOnExit() и Runtime.runFinalizersOnExit() |
| Java 18 | JEP 421: финализация помечена deprecated for removal; появился ключ запуска --finalization=disabled, полностью отключающий вызовы finalize() |
| Будущие релизы | Финализация будет отключена по умолчанию, а затем удалена из платформы вместе с методом finalize() |
Практический вывод: переопределять finalize() в новом коде нельзя. Знать о нём нужно, чтобы понимать легаси-код и отвечать на вопросы собеседования.
6. Чем заменить finalize(): try-with-resources и Cleaner
Основной способ освобождать ресурсы в Java — детерминированный: класс реализует AutoCloseable, а пользователь класса открывает его в try-with-resources. Тогда close() вызывается сразу по выходе из блока, независимо от сборщика мусора.
java.lang.ref.Cleaner (появился в Java 9) нужен только как страховка: он выполнит действие по очистке, если объект стал мусором, а close() так и не вызвали. В отличие от finalize(), действие очистки не имеет доступа к самому объекту, выполняется в выделенном потоке и не мешает сборке.
import java.lang.ref.Cleaner;
public class Cup implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
// Состояние не должно ссылаться на Cup, иначе объект никогда не станет мусором
private static class State implements Runnable {
@Override
public void run() {
System.out.println("Чашка вымыта: ресурсы освобождены");
}
}
private final Cleaner.Cleanable cleanable;
public Cup() {
this.cleanable = CLEANER.register(this, new State());
}
@Override
public void close() {
cleanable.clean(); // очистка выполнится ровно один раз
}
public static void main(String[] args) {
try (Cup cup = new Cup()) {
System.out.println("Наливаем чай");
}
}
} Наливаем чай
Чашка вымыта: ресурсы освобождены Неочевидный момент
Класс состояния в Cleaner обязан быть статическим вложенным (или отдельным) и не хранить ссылку на владельца. Нестатический вложенный класс и лямбда, захватывающая this, создают сильную ссылку на объект — он никогда не станет недостижимым, и очистка не выполнится никогда.
Сравнение доступных механизмов:
| Механизм | Когда срабатывает | Детерминированность | Статус |
|---|---|---|---|
try-with-resources + AutoCloseable | На выходе из блока try | Полная, момент известен точно | Рекомендуемый способ (Java 7+) |
Явный close() в finally | Там, где написан вызов | Полная, но легко забыть | Легаси-код до Java 7 |
java.lang.ref.Cleaner | После того как объект стал недостижим | Нет, только страховка | Java 9+, замена finalize() |
PhantomReference + ReferenceQueue | После сборки объекта, вручную из очереди | Нет, требует своего потока | Низкоуровневый вариант Cleaner |
finalize() | Неизвестно, возможно никогда | Отсутствует | Deprecated for removal (Java 18) |
7. Типы ссылок: strong, soft, weak, phantom
Пакет java.lang.ref позволяет управлять тем, насколько сильно ссылка удерживает объект от сборки. Эту тему часто спрашивают на собеседовании вместе с finalize().
| Тип ссылки | Класс | Когда объект может быть удалён | Где применяется |
|---|---|---|---|
| Strong (обычная) | — | Никогда, пока ссылка достижима от GC roots | Весь обычный код |
| Soft | SoftReference | При нехватке памяти, до выброса OutOfMemoryError | Кэши, которые не жалко потерять |
| Weak | WeakReference | При первой же сборке, если сильных ссылок нет | WeakHashMap, метаданные, слушатели |
| Phantom | PhantomReference | Уже удалён; get() всегда возвращает null | Освобождение нативных ресурсов, основа Cleaner |
8. Какие сборщики мусора есть в JVM
Сборщик мусора в HotSpot выбирается флагом запуска. Начиная с Java 9 по умолчанию используется G1.
| Сборщик | Флаг | Особенности | Когда выбирать |
|---|---|---|---|
| Serial | -XX:+UseSerialGC | Однопоточный, минимальные накладные расходы | Маленькая куча, контейнер с одним ядром |
| Parallel | -XX:+UseParallelGC | Максимальная пропускная способность, длинные паузы | Пакетная обработка, где паузы не критичны |
| G1 | -XX:+UseG1GC | По умолчанию с Java 9, куча из регионов, целевая длительность пауз | Большинство серверных приложений |
| ZGC | -XX:+UseZGC | Паузы порядка долей миллисекунды на кучах в терабайты | Сервисы, чувствительные к задержкам |
| Shenandoah | -XX:+UseShenandoahGC | Уплотнение кучи параллельно с работой приложения | Низкие задержки, сборки OpenJDK |
| Epsilon | -XX:+UseEpsilonGC | Ничего не собирает, память просто заканчивается | Бенчмарки и короткоживущие задачи |
9. Что чаще всего понимают неправильно
- «Есть GC — значит, утечек памяти нет». Утечка в Java — это объект, который больше не нужен, но всё ещё достижим: элемент в статической коллекции, неотписанный слушатель, ключ в
HashMapбез корректныхequals()/hashCode(), поток из пула, удерживающийThreadLocal. - «
cup = nullудаляет объект». Присваиваниеnullлишь убирает одну ссылку. Память освободит сборщик мусора, и только если других ссылок не осталось. - «
System.gc()запускает сборку». Это запрос, а не команда, и его можно отключить флагом JVM. - «
finalize()— это деструктор». Деструктора в Java нет: деструктор в C++ вызывается детерминированно,finalize()— нет. Ближайший аналог по смыслу —close()в try-with-resources. - «Циклические ссылки не собираются». Собираются: JVM опирается на достижимость, а не на подсчёт ссылок.
- «
finalize()вызовется хотя бы при выходе из программы». Нет: методrunFinalizersOnExit(), который это обещал, удалён в Java 11 как небезопасный.
Часто задаваемые вопросы
Какой сборщик мусора используется в Java по умолчанию?
Начиная с Java 9 по умолчанию работает G1 (Garbage-First); в Java 8 по умолчанию был Parallel GC. На слабых машинах JVM по эргономике может выбрать Serial GC. Проверить текущий выбор можно командой java -XX:+PrintFlagsFinal -version или в логах запуска с ключом -Xlog:gc.
Бывают ли утечки памяти в Java, если есть сборщик мусора?
Да. Сборщик мусора удаляет только недостижимые объекты, поэтому утечка в Java — это ненужный объект, который остаётся достижимым. Типичные источники: растущие статические коллекции, кэши без вытеснения, неотписанные слушатели, ThreadLocal в пуле потоков, незакрытые ресурсы. Диагностируют такие утечки по heap dump в VisualVM, Eclipse MAT или через Java Flight Recorder.
Чем PhantomReference отличается от finalize()?
Фантомная ссылка не даёт доступа к объекту: её метод get() всегда возвращает null, поэтому воскресить объект невозможно. Уведомление приходит в ReferenceQueue уже после того, как объект признан недостижимым, и вы обрабатываете его в своём потоке в удобный момент. На фантомных ссылках построен класс Cleaner, который и стоит использовать вместо ручной работы с очередью.
Как полностью отключить финализацию в JDK 18 и новее?
Запустите приложение с ключом --finalization=disabled. Тогда JVM не вызовет ни один метод finalize(), даже переопределённый. Это удобный способ заранее проверить готовность к будущему удалению финализации: если после отключения ничего не сломалось, зависимости от finalize() в вашем коде и библиотеках нет.
Можно ли по finalize() посчитать, сколько объектов удалено?
Нет, такой счётчик будет неверным. Метод вызывается не для всех объектов, может не вызваться совсем, срабатывает максимум один раз и в непредсказуемый момент. Для подсчёта объектов и анализа памяти используйте инструменты JVM: jcmd GC.class_histogram, heap dump или события Java Flight Recorder.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии