GC и ноль утечек: погребальный обряд старого поколения
Что происходит после обнуления lease? Выделенный поток в фиксированном порядке разбирает старое поколение — глава о том, как убедиться, что разобрано оно действительно начисто.
Обзор
30.1 Введение
У сборки мусора горячей перезагрузки есть природная трудность: судьба старого поколения зависит от времени завершения асинхронных событий (когда оборвутся соединения, когда вернутся колбэки) — в среде многих worker непредсказуемо. В истории XServer реально бытовали две стратегии: ранние версии выбирали «старое поколение течёт целиком» (не освобождать, абсолютная безопасность); действующая версия эволюционировала в «нуль ссылок + сигнал безопасности + отложенное освобождение» — сигналом безопасности служит обнуление счётчика соединений старого драйвера (все Close-колбэки завершены), после чего за широкой паузой идёт поэтапное освобождение. Причина эволюции проста: при стратегии утечек частые перезагрузки съедают всю память.
30.2 Reaper и порядок деструкции
Смысл порядка: ваш ServiceUnit выполняется до сноса любых ресурсов хоста — при очистке инфраструктура хоста ещё доступна; и выполняется после того, как старое поколение уже не обслуживает — очистка не задевает ни один запрос. Эта позиция спроектирована.
30.3 Замеренный кейс
В истории репозитория записан приёмочный эксперимент этого механизма (трёхцикловая смена поколений 8080↔8081): одна и та же группа портов трижды меняет поколения, с утверждениями —
- каждый раунд обслуживает безупречно (ноль отказов запросов)
- все 6 старых поколений финализируются (счёт логов сходится с числом смен)
- ноль утечек (отчёты памяти до и после совпадают), ноль падений, без force-free
Ваш сервис можно принимать той же триадой: N смен под нагрузкой → N finalized в логе → ровная кривая RSS (способ замера drill). Эти утверждения зафиксированы в lifecycle_reload_test и reload_matrix (таблица базлайнов гл. 06).
30.4 Таймеры умирают со своим поколением
В порядке GC спрятано важное для приложений следствие: таймеры старого поколения устаревают в момент смены, а не ждут GC. Механически таймер привязан к поколению, которое его создало (строго — к потоково-локальной метке «исполняющегося старого поколения»), — после смены таймеры старого поколения отменяются, а новое перевешивает свои из Init.
- Результат: таймер никогда не выстрелит не в своё поколение — статики, к которым колбэк обращается, всегда принадлежат создавшему его поколению
- Цена: периодические задачи обязаны пересоздаваться в Init нового поколения (яма файлового триггера из гл. 27 — ровно это)
- Выстрел уже в полёте: начавший исполняться колбэк защищён lease своего поколения до завершения
30.5 Контракт
- Порядок: ServiceUnit → TCC → драйвер → конфигурация, неизменен.
- Наблюдаемость: счёт finalized = числу смен поколений; появление force-free — дефект.
- Таймеры: привязаны к создавшему поколению; устаревают со сменой; периодические задачи пересоздаются в новом Init.
Частые ошибки
- Симптом
- После нескольких смен копится очередь GC; лог finalize отстаёт от смен.
- Причина
- Поток reaper исполняет Unit-ы поколений последовательно — один Unit, застрявший на 30 секунд, ставит в очередь всех остальных. Очистка должна быть быстрой (закрыть дескрипторы, сбросить буферы); долгую работу — в фоновый поток под самоуправление.
- Симптом
- finalized появляется через несколько минут после смены; подозрение на утечку памяти.
- Причина
- Длинные соединения (WS/TAKEOVER) держат lease старого поколения — finalize ждёт их конца (таблица lease гл. 29). Сначала соединения, потом утечки.
Упражнения
Подряд смените поколение 5 раз; посчитайте строки finalized и сверьте с числом смен.
Повторите эксперимент 30.3: три цикла смен под нагрузкой + кривая замеров RSS.
Старое поколение вешает периодическую задачу на 10 с (печать маркера поколения); на 3-й секунде смена — пронаблюдайте остановку старой задачи и продолжение после пересоздания новым.
Итоги
| Тезис | Вывод |
|---|---|
| Эволюция стратегии | ранняя полная утечка → нуль ссылок + сигнал безопасности + отложенное освобождение |
| Порядок деструкции | ServiceUnit → TCC → драйвер → конфигурация (фиксирован) |
| Наблюдаемость | сверка счёта finalized; force-free = сигнал дефекта |
| Таймеры | привязка к создавшему поколению, устаревание со сменой, пересоздание в Init |
- справочник xrt · memory_debug — отчёты распределений для сверки утечек
- учебник xrt · отладка памяти — полное применение канареек / UAF / журнала событий