Сборка мусора Unreal — Mark-and-Sweep по графу UPROPERTY
Сборщик мусора в Unreal Engine принципиально отличается от того, к чему привык программист на «обычном» C++. Здесь нет подсчёта ссылок, нет std::shared_ptr, нет деструктора в момент потери последней ссылки. Есть периодический трассирующий сборщик: каждые несколько секунд движок останавливает мир, обходит граф достижимости от корневого набора по полям, отмеченным макросом UPROPERTY, и зачищает всё, до чего не дошёл. Это и есть Mark-and-Sweep.
Из этой модели вытекают все остальные правила. Сильная ссылка — это UPROPERTY-поле, держащее UObject* или TObjectPtr<>: GC видит его при трассировке и продлевает жизнь цели. Сырой UObject* Cached в обычном C++-классе — невидим для сборщика: он не удерживает объект и через цикл GC превращается в висячий указатель. Слабая ссылка TWeakObjectPtr объект не удерживает вовсе, но в отличие от сырого указателя знает, что цель умерла, и честно отвечает IsValid() == false.
Третий ключевой момент — циклы решаются автоматически. Два UObject, держащие друг друга через UPROPERTY, не создают утечку: если на этот цикл нет пути из корневого набора, обе вершины недостижимы и обе будут собраны в одну фазу зачистки. Это главное преимущество трассирующего GC над подсчётом ссылок и причина, почему TWeakObjectPtr в UE — это инструмент для семантики «не владею», а не для разрыва циклов, как std::weak_ptr в STL.
Карта темы
- Object Lifetime —
NewObject, Outer chain,Destroy,MarkAsGarbage,BeginDestroy,FinishDestroy, фазы цикла GC. - GC Algorithm — фаза маркировки, фаза зачистки, обход графа по
UPROPERTY, реестр объектовGUObjectArray. - GC Root Set — что входит в корневой набор по умолчанию,
AddToRoot/RemoveFromRoot, опасности «постоянного» рута. - Strong References —
UPROPERTY,TObjectPtr<>,TArray<TObjectPtr<>>, auto-null при смерти цели. - Weak References —
TWeakObjectPtr,IsValid, кэши и обратные указатели на владельца. - Validity Check —
IsValidvsIsValidLowLevelvsnullptr-сравнение, что они проверяют и когда лгут. - UObject GC — UE GC vs
std::shared_ptr, incremental GC, кластеризация, что не управляется сборщиком.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Хранить UObject* Cached; без UPROPERTY в C++-классе | Висячий указатель после следующего цикла GC; падение в случайном кадре |
Полагать, что GC работает как std::shared_ptr | Ожидание подсчёта ссылок; недоумение, почему объект жив |
Считать UPROPERTY ссылку на цикл утечкой | Mark-and-Sweep свободно собирает циклы; утечки нет |
Звать AddToRoot «на всякий случай» | Постоянная утечка: укоренённый объект и всё, на что он ссылается, никогда не собираются |
Сравнивать с nullptr вместо IsValid | UObject* Ptr ненулевой, но цель уже PendingKill — крэш в ->Method() |
Звать delete на UObject | Поломка реестра GC; падения и повреждение памяти |
Использовать Actor сразу после Destroy() | Destroy лишь помечает объект; реальная зачистка позже — типичная ловушка |
Хранить сырой UObject* в не-UObject классе (FMyStruct, FCallback) | GC не видит ссылку; объект погибает, указатель висит |
Использовать TWeakObjectPtr чтобы «разорвать цикл» | Циклы и так разрываются Mark-and-Sweep; TWeakObjectPtr нужен для семантики «не владею» |
Полагаться на IsValidLowLevel в шиппинг-коде | Это диагностика, не валидатор; в release ведёт себя иначе и не заменяет IsValid |
Значение для собеседований
Сборка мусора — обязательная middle-тема для любого UE5-собеседования. Проверяется:
- Что GC в UE — Mark-and-Sweep, а не подсчёт ссылок, и что циклы собираются автоматически.
- Чем
UPROPERTYпринципиально отличается от сырогоUObject*в C++-классе. - Что такое корневой набор и почему
AddToRootбезRemoveFromRoot— постоянная утечка. - Разницу между
TObjectPtr<>(сильная) иTWeakObjectPtr<>(слабая). - Что проверяет
IsValidи почему сравнение сnullptrнедостаточно. - Почему
Destroy()на Actor — это неdelete, и что происходит между ними и реальным освобождением памяти.
Типичный неверный ответ: «GC в Unreal работает как умные указатели: пока на объект кто-то ссылается — он жив». Это ложь дважды: во-первых, GC трассирующий, а не считающий; во-вторых, «кто-то ссылается» через сырой UObject* для GC не существует — нужна ссылка через UPROPERTY или элемент корневого набора.