Сборка мусора
Поколенческая сборка, System.gc(), низколатентные сборщики, типы ссылок, утечки памяти при наличии GC, настройка и диагностика сборки мусора.
10 вопросов
JuniorТеорияОчень частоВ чём разница между young и old поколением в heap JVM?
В чём разница между young и old поколением в heap JVM?
Большинство объектов умирает молодыми — слабая поколенческая гипотеза. Young gen (eden и два survivor-пространства) очищается частыми дешёвыми minor GC, копирующими немногих выживших; они продвигаются в old gen, собираемый редко, но дорого.
Типичные ошибки
- ✗Думать, что все объекты живут примерно одинаково, и поколения ничего не дают
- ✗Считать, что old gen собирается так же часто и дёшево, как young
- ✗Полагать, что новые объекты выделяются сразу в old gen
Уточняющие вопросы
- →Что запускает продвижение объекта из survivor-пространств в old gen?
- →Почему young gen использует два survivor-пространства, а не одно?
JuniorТеорияОчень частоЧто такое сборка мусора (GC) в Java и какую проблему она решает?
Что такое сборка мусора (GC) в Java и какую проблему она решает?
Сборка мусора — это автоматическое освобождение JVM объектов в heap, до которых больше нет ни одной живой ссылки. Она избавляет разработчика от ручного управления памятью — нет free или delete, — что предотвращает большинство утечек и висячих указателей. Цена в том, что время сборки недетерминировано: нельзя предсказать, когда именно объект будет освобождён.
Типичные ошибки
- ✗Считать, что
GCсрабатывает детерминированно в тот же миг, когда объект становится недостижим - ✗Думать, что автоматическое управление памятью делает утечки
heapв Java невозможными - ✗Искать ручной
free/delete, которых Java намеренно не предоставляет
Уточняющие вопросы
- →Если ссылки всё ещё удерживаются, может ли объект утечь несмотря на сборку мусора?
- →Как сборщик решает, что объект недостижим, а не просто не используется?
MiddleДебаггингЧастоСтатический кэш растёт без ограничений несмотря на сборщик мусора
Статический кэш растёт без ограничений несмотря на сборщик мусора
Статическая final карта — это GC root, держащий сильную ссылку на каждый разобранный Session, поэтому ни одна запись не становится недостижимой и сборщик не может её освободить: это утечка по достижимости, а не сбой GC. Лечится ограничением кэша — LRU через LinkedHashMap.removeEldestEntry или WeakHashMap, чьи записи очищаются, как только не остаётся сильных ссылок на ключ.
Типичные ошибки
- ✗Считать, что сборщик мусора делает утечку памяти в Java невозможной
- ✗Винить сборщик или размер поколений, когда объект по-прежнему достижим
- ✗Считать
System.gc()или увеличение кучи лекарством от неограниченного кэша
Уточняющие вопросы
- →Как heap dump покажет, какой именно GC root удерживает сессии?
- →Когда
WeakHashMap— неверный кэш, а ограниченный LRU — верный?
MiddleТеорияЧастоВ чём разница между сильными, мягкими, слабыми и фантомными ссылками в Java?
В чём разница между сильными, мягкими, слабыми и фантомными ссылками в Java?
Сильная ссылка — по умолчанию — держит объект достижимым и не даёт его собрать. SoftReference очищается только под нехваткой памяти, поэтому подходит для чувствительных к памяти кэшей. WeakReference очищается на ближайшей сборке, как только не осталось ни одной сильной ссылки, — так работают ключи WeakHashMap. PhantomReference нельзя разыменовать (get() всегда возвращает null); она лишь ставится в ReferenceQueue после сборки — безопасный хук очистки вместо finalize().
Типичные ошибки
- ✗Считать, что
WeakReferenceочищается лишь под нехваткой памяти, какSoftReference - ✗Ожидать, что
PhantomReference.get()вернёт объект, а не всегдаnull - ✗Считать soft и weak ссылки взаимозаменяемыми для кэширования
Уточняющие вопросы
- →Когда кэш на
SoftReferenceвсё же приведёт кOutOfMemoryError? - →Почему
PhantomReferenceсReferenceQueueбезопаснее, чемfinalize()?
JuniorТеорияИногдаГарантирует ли вызов System.gc(), что сборка мусора выполнится?
Гарантирует ли вызов System.gc(), что сборка мусора выполнится?
Нет — System.gc() лишь подсказка. JVM вправе её проигнорировать, а -XX:+DisableExplicitGC делает вызов no-op. Вызов в прикладном коде обычно запускает дорогой full GC и бьёт по throughput; это не способ освободить память или устранить утечку.
Типичные ошибки
- ✗Считать, что
System.gc()форсирует немедленную гарантированную сборку - ✗Вызывать его в проде, чтобы освободить память или устранить подозреваемую утечку
- ✗Полагать, что он дёшев, а не обычно полный stop-the-world GC
Уточняющие вопросы
- →Почему
-XX:+DisableExplicitGCможет быть полезным флагом в проде? - →Если не
System.gc(), как правильно подступиться к подозреваемой утечке?
MiddleДебаггингИногдаОпределите по выводу jstat, почему сервис проводит почти всё время в GC
Определите по выводу jstat, почему сервис проводит почти всё время в GC
Занятость old gen O держится выше 97% и не падает после full GC, а FGC и FGCT растут круто — это идущие подряд полные сборки, которые почти ничего не освобождают. Признак удерживаемого живого набора (утечки), а не малого young gen. Подтверждают heap dump (jmap -dump), разобранный в Eclipse MAT или VisualVM, — его dominator tree называет удерживающий GC root, — плюс -Xlog:gc* или JFR для истории пауз.
Типичные ошибки
- ✗Читать высокую занятость old gen как норму, а не как удерживаемый живой набор
- ✗Крутить флаги кучи, не подтвердив heap dump'ом, что именно удерживается
- ✗Считать растущее число полных сборок нормой для любого долгоживущего сервиса
Уточняющие вопросы
- →Что dominator tree в Eclipse MAT скажет такого, чего не скажет
jstat? - →Как выглядел бы тот же вывод, если бы young gen действительно был мал?
SeniorТеорияИногдаКакие сборщики мусора (GC) предлагает JVM, и чем они различаются?
Какие сборщики мусора (GC) предлагает JVM, и чем они различаются?
Serial использует один поток, годится для малых куч. Parallel использует много потоков, максимизируя пропускную способность. CMS был конкурентным, но теперь удалён. G1 (garbage-first) — сборщик по умолчанию: порегионный, балансирует пропускную способность и предсказуемые паузы. ZGC и Shenandoah дают очень низкие паузы, не зависящие от размера кучи. Ключевой компромисс — пропускная способность против времени паузы.
Типичные ошибки
- ✗Считать один сборщик строго лучшим вместо компромисса пропускная способность–пауза
- ✗Называть CMS текущим вариантом, хотя его удалили из современных JVM
- ✗Считать каждый сборщик полностью stop-the-world, а не конкурентным или порегионным
Уточняющие вопросы
- →Как порегионная структура
G1позволяет ему укладываться в целевое время паузы? - →Какая техника позволяет
ZGCдержать паузы менее миллисекунды даже на многотерабайтных кучах?
MiddleПроизводительностьРедкоКак низколатентный сборщик ZGC держит stop-the-world (STW) паузы менее миллисекунды?
Как низколатентный сборщик ZGC держит stop-the-world (STW) паузы менее миллисекунды?
ZGC выполняет маркировку, перемещение объектов и переадресацию ссылок конкурентно с работающим приложением. Цветные указатели хранят состояние GC в незанятых битах адреса, а load barrier на каждом чтении ссылки лениво переадресует устаревший указатель. Stop-the-world остаются лишь короткие сканы корней, поэтому пауза зависит от размера корневого набора, а не от размера кучи.
Типичные ошибки
- ✗Считать, что
ZGC— это простоG1с бо́льшим числом потоков GC, а не конкурентный сборщик - ✗Полагать, что пауза
ZGCрастёт с размером кучи, как у старых сборщиков - ✗Путать load barrier на чтении с write barrier или считать, что барьера нет вовсе
Уточняющие вопросы
- →Какая работа у
ZGCвсё же остаётся внутри stop-the-world паузы? - →Почему
ZGCжертвует частью пропускной способности ради субмиллисекундных пауз?
SeniorДебаггингРедкоОпределите причину растущих stop-the-world (STW) пауз GC в проде
Определите причину растущих stop-the-world (STW) пауз GC в проде
G1 Humongous Allocation помечает объекты крупнее половины региона: они выделяются сразу в old gen и собираются плохо. Они фрагментируют кучу, пока эвакуации не остаётся ни одного свободного региона (To-space exhausted), и G1 откатывается к полной stop-the-world компактизации — отсюда многосекундные паузы. Целевое время паузы не переживает откат к full GC. Правьте саму аллокацию, увеличьте -XX:G1HeapRegionSize и снизьте InitiatingHeapOccupancyPercent, чтобы конкурентный цикл стартовал раньше.
Типичные ошибки
- ✗Читать
To-space exhaustedкак проблему размера survivor, а не нехватку свободных регионов - ✗Поднимать целевое время паузы вместо устранения слишком крупных аллокаций
- ✗Считать, что растущая после сборки занятость всегда означает обычную утечку
Уточняющие вопросы
- →Почему объект крупнее половины региона
G1минует young gen? - →Избежал бы низколатентный
ZGCэтого сбоя, и какой ценой?
SeniorДебаггингРедкоРазберите медленную утечку памяти Spring Boot-сервиса по гистограмме кучи
Разберите медленную утечку памяти Spring Boot-сервиса по гистограмме кучи
Гистограмма показывает, что AuditEntry — и удерживаемые им byte[]/String — растут без ограничений синхронно с числом запросов, удерживаемые через HashMap$Node. Бин аудита копит по записи на запрос и ничего не вытесняет. Singleton-бин Spring живёт столько же, сколько контекст приложения, поэтому всё, что он накопил, остаётся достижимым, и GC правомерно это не освобождает. Подтвердите путь удержания по dominator tree в heap dump и ограничьте буфер размером или политикой вытеснения.
Типичные ошибки
- ✗Считать коллекцию внутри singleton-бина короткоживущим состоянием уровня запроса
- ✗Поднимать
-Xmxили менять сборщик ради отсрочки вместо поиска удерживающего корня - ✗Читать самую большую строку гистограммы (
byte[]) как утечку, а не как то, что удерживается
Уточняющие вопросы
- →Почему удерживающего владельца называет dominator tree, а не гистограмма?
- →Как ограничить буфер, не потеряв записи аудита, нужные админской странице?