Сборка мусора
Сборщик мусора Unreal Engine — как он трассирует ссылки на UObject, удержание объектов живыми, слабые ссылки, владение и отладка падений из-за висячих указателей.
16 вопросов
JuniorТеорияОчень частоКак проверить, что объект UObject всё ещё валиден и пригоден к использованию?
Как проверить, что объект UObject всё ещё валиден и пригоден к использованию?
Используйте IsValid(Obj) — она проверяет, что указатель не нулевой и объект не помечен на уничтожение. Для TWeakObjectPtr вызовите .IsValid(). Простой проверки на ненулевой указатель недостаточно — он может быть висячим.
Типичные ошибки
- ✗Полагаться только на проверку
!= nullptr— не-UPROPERTY()указатель может быть висячим - ✗Думать, что
IsValid()точна только после прохода GC - ✗Использовать
IsValidLowLevel()как общую проверку валидности в игровом коде
Уточняющие вопросы
- →Почему сырой
UObject*может быть ненулевым, но всё равно невалидным? - →Что именно проверяет
IsValid()помимо проверки на null?
JuniorТеорияОчень частоЧто такое корневой набор сборщика мусора в Unreal?
Что такое корневой набор сборщика мусора в Unreal?
Корневой набор — это UObject, которые сборщик мусора считает всегда достижимыми: помещённые в root объекты и ключевые объекты движка. GC стартует от корневого набора и сохраняет всё, что достижимо из него; недостижимые объекты уничтожаются.
Типичные ошибки
- ✗Думать, что каждый существующий UObject автоматически входит в корневой набор
- ✗Считать корневой набор статичным и неизменяемым во время работы игры
- ✗Путать корневой набор со всеми объектами, достижимыми из него
Уточняющие вопросы
- →Как указатель
UPROPERTY()сохраняет объект достижимым от корня? - →Что делает
AddToRoot()по отношению к корневому набору?
MiddleТеорияОчень частоВ чём разница между сильными и слабыми ссылками в Unreal?
В чём разница между сильными и слабыми ссылками в Unreal?
Сильная ссылка (UPROPERTY() указатель или TObjectPtr) удерживает цель живой и обнуляется, если та умирает. Слабая ссылка (TWeakObjectPtr) цель не удерживает и сообщает о невалидности, поэтому перед использованием её надо проверять.
Типичные ошибки
- ✗Думать, что
TWeakObjectPtrудерживает свою цель живой - ✗Считать, что слабый указатель обнуляется — он сообщает о невалидности, слот не превращается в записанный null
- ✗Верить, что сильная и слабая ссылки отличаются лишь видимостью в редакторе
Уточняющие вопросы
- →Почему
TWeakObjectPtrне создаёт утечку из-за циклической ссылки? - →Чем сильный
TObjectPtrведёт себя иначе, чем сырой указательUPROPERTY()?
JuniorТеорияЧастоЧто такое TWeakObjectPtr и когда его использовать?
Что такое TWeakObjectPtr и когда его использовать?
TWeakObjectPtr — это невладеющий указатель на UObject, который не препятствует сборке мусора. Используйте его для ссылки на чужой объект, а перед доступом проверяйте IsValid() — после уничтожения цели он сообщает невалидность, а не повисает.
Типичные ошибки
- ✗Считать, что TWeakObjectPtr удерживает свою цель живой против GC
- ✗Обращаться к указателю без предварительной проверки IsValid() на нём
- ✗Использовать слабый указатель для объекта, которым класс владеет
Уточняющие вопросы
- →Чем
TWeakObjectPtrотличается от указателяUPROPERTY()? - →Что возвращает
IsValid()после уничтожения целевого объекта?
MiddleТеорияЧастоЧто такое AddToRoot() и почему злоупотребление им опасно?
Что такое AddToRoot() и почему злоупотребление им опасно?
AddToRoot() добавляет UObject напрямую в корневой набор GC, поэтому он живёт, пока вы не вызовете RemoveFromRoot(). Злоупотребление создаёт постоянные утечки: укоренённые объекты и всё, на что они ссылаются, никогда не собираются.
Типичные ошибки
- ✗Думать, что укоренение временно или авто-истекает — оно держится до
RemoveFromRoot() - ✗Забывать, что укоренение также удерживает живым всё, на что объект ссылается
- ✗Использовать
AddToRoot()там, где хватило бы членаUPROPERTY()
Уточняющие вопросы
- →Как бы вы обнаружили объект, который укоренили и не открепили?
- →Когда
AddToRoot()действительно правильный инструмент вместоUPROPERTY()?
MiddleТеорияЧастоКак в Unreal Engine могут возникнуть циклические ссылки?
Как в Unreal Engine могут возникнуть циклические ссылки?
Два UObject, держащие сильные указатели UPROPERTY() друг на друга, образуют цикл. В отличие от подсчёта ссылок, mark-and-sweep GC в UE всё равно собирает цикл, если он недостижим из корневого набора, поэтому утечки нет.
Типичные ошибки
- ✗Думать, что UE утекает циклы ссылок, как система на подсчёте ссылок
- ✗Считать, что циклы важны только между Actor'ами или чистятся при загрузке карты
- ✗Путать слабый обратный указатель (решение) с причиной циклов
Уточняющие вопросы
- →Почему mark-and-sweep устойчив к утечкам циклов, от которых страдает подсчёт ссылок?
- →Когда цикл из двух сильных ссылок всё же вызовет практическую проблему?
MiddleТеорияЧастоКаковы частые причины висячих указателей в Unreal?
Каковы частые причины висячих указателей в Unreal?
Хранение UObject* без UPROPERTY() (GC не может его обнулить), кэширование сырого указателя между кадрами или использование Actor после Destroy(). Не-UObject классы, держащие сырой UObject*, — ещё один частый источник.
Типичные ошибки
- ✗Думать, что указатель
UPROPERTY()вызывает висячесть — наоборот, он её предотвращает - ✗Считать, что
NewObjectпереиспользует память так, что живые указатели начинают алиаситься - ✗Полагать, что указатель на Actor безопасно хранить после вызова
Destroy()
Уточняющие вопросы
- →Как
UPROPERTY()превращает потенциальную висячесть в безопасный null? - →Почему не-
UObjectкласс, держащийUObject*, особенно рискован?
MiddleТеорияЧастоЧем Destroy() у Actor отличается от C++ delete?
Чем Destroy() у Actor отличается от C++ delete?
Destroy() помечает Actor на удаление — он покидает мир, но память освобождается позже сборщиком мусора, а не сразу. delete освобождает память сразу; вызов на UObject портит состояние GC, поэтому UObject нельзя удалять через delete.
Типичные ошибки
- ✗Думать, что
Destroy()освобождает память немедленно - ✗Считать, что
deleteнаUObjectдопустим хоть в каком-то случае - ✗Полагать, что указатель на Actor безопасен сразу после
Destroy()
Уточняющие вопросы
- →Между
Destroy()и фактическим освобождением — Actor ещё валиден? - →Почему движок запрещает
deleteнаUObject, а не просто не рекомендует?
MiddleТеорияЧастоКак сборщик мусора Unreal Engine устроен и работает внутри?
Как сборщик мусора Unreal Engine устроен и работает внутри?
GC — это периодический трассирующий сборщик типа mark-and-sweep. От корневого набора он идёт по ссылкам UPROPERTY(), помечая все достижимые UObject; недостижимые помечаются на уничтожение и удаляются. Запускается по таймеру, а не на каждое выделение.
Типичные ошибки
- ✗Думать, что GC в UE использует подсчёт ссылок, как
shared_ptr - ✗Считать, что GC сканирует сырые указатели, а не отражённые ссылки
UPROPERTY() - ✗Предполагать, что сбор происходит сразу, когда объект становится недостижимым
Уточняющие вопросы
- →Что такое корневой набор и как объект в него попадает?
- →Как движок делает полный проход GC достаточно дешёвым, чтобы он уместился в кадр?
MiddleТеорияЧастоКак не дать сборщику мусора уничтожить нужный объект UObject?
Как не дать сборщику мусора уничтожить нужный объект UObject?
Сделайте его достижимым из корневого набора: храните в UPROPERTY(), добавьте в член TArray<TObjectPtr<>> или вызовите AddToRoot(), чтобы сделать его корнем напрямую. Объект без пути сильных ссылок будет собран.
Типичные ошибки
- ✗Думать, что обычный
UObject*удерживает объект — учитываются только ссылкиUPROPERTY() - ✗Путать
MarkPendingKill()(уничтожить) с удержанием живым - ✗Считать, что
TWeakObjectPtrпредотвращает сбор
Уточняющие вопросы
- →Когда бы вы выбрали
AddToRoot()вместо ссылкиUPROPERTY()? - →Как
UObject, созданный черезNewObject, избегает сбора до того, как вы его сохраните?
MiddleТеорияЧастоВсегда ли указатель UPROPERTY() удерживает свой целевой объект живым?
Всегда ли указатель UPROPERTY() удерживает свой целевой объект живым?
Нет. Сильный указатель UPROPERTY() удерживает цель живой только пока сам владеющий объект достижим. Это также не относится к слабым членам UPROPERTY() вроде TWeakObjectPtr, которые цель не удерживают вовсе.
Типичные ошибки
- ✗Думать, что указатель
UPROPERTY()— постоянный корень независимо от владельца - ✗Забывать, что
UPROPERTY()TWeakObjectPtr— слабый и ничего не удерживает - ✗Считать, что один лишь ненулевой указатель защищает объект от GC
Уточняющие вопросы
- →Если владеющий объект собран, что происходит с объектами, на которые он ссылался через
UPROPERTY()? - →Как работает цепочка достижимости от корневого набора вниз до листового объекта?
MiddleТеорияЧастоКогда следует использовать TWeakObjectPtr вместо сильной ссылки?
Когда следует использовать TWeakObjectPtr вместо сильной ссылки?
Используйте, когда нужно ссылаться на объект, не управляя его временем жизни — кэши, наблюдатели или обратные указатели на владельца. Это избегает утечек из-за циклических ссылок и позволяет безопасно узнать, что цель уничтожена.
Типичные ошибки
- ✗Использовать
TWeakObjectPtrповсюду в расчёте на выигрыш в производительности - ✗Думать, что слабый указатель продлевает время жизни через загрузки уровней
- ✗Путать выбор между слабой и сильной ссылкой с вопросами потокобезопасности
Уточняющие вопросы
- →Как безопасно разыменовать
TWeakObjectPtrперед использованием цели? - →Почему ребёнок, держащий сильный указатель
UPROPERTY()обратно на родителя, — это проблема?
JuniorТеорияИногдаЧем управляет сборка мусора в UE — охватывает ли она сырой new?
Чем управляет сборка мусора в UE — охватывает ли она сырой new?
GC управляет только типами, производными от UObject (Actor, компоненты, UObject). Память от сырого new, malloc или не-UObject структур вроде FString ей невидима — её нужно освобождать самому или использовать умный указатель.
Типичные ошибки
- ✗Думать, что GC освобождает выделения от сырого
new/malloc - ✗Считать, что
FString,TArrayи обычныеUSTRUCTсобираются мусором - ✗Верить, что утечка памяти в проекте Unreal невозможна
Уточняющие вопросы
- →Как управляется время жизни члена-обычного
USTRUCT? - →Почему не-
UObjectкласс всё же может безопасно хранить ссылку наUObject?
MiddleТеорияИногдаЧто такое владение объектами в Unreal Engine и что такое Outer?
Что такое владение объектами в Unreal Engine и что такое Outer?
Каждый UObject имеет Outer — объект, который им владеет, задаваемый при создании через NewObject. Цепочка Outer задаёт область видимости и именование; если Outer собран, внутренние объекты теряют достижимость и тоже собираются.
Типичные ошибки
- ✗Путать Outer с владельцем на основе подсчёта ссылок
- ✗Думать, что Outer — лишь метка редактора без влияния на время жизни
- ✗Считать, что объект может пережить свой Outer
Уточняющие вопросы
- →Как выбрать правильный Outer при вызове
NewObject? - →В чём разница между Outer и ссылкой
UPROPERTY()на объект?
SeniorДебаггингИногдаКак отлаживать падение из-за невалидной ссылки на UObject?
Как отлаживать падение из-за невалидной ссылки на UObject?
Прочитайте стек вызовов, найдите разыменованный указатель и проверьте, является ли он UPROPERTY(). Обычная причина — неотражённый указатель, повисший после GC; решение — добавить UPROPERTY() или применить TWeakObjectPtr с проверкой IsValid.
Типичные ошибки
- ✗Убирать
UPROPERTY(), чтобы «GC не обнулял», — именно это и вызывает висячесть - ✗Считать, что падение зависит от конфигурации сборки, а не настоящий баг времени жизни
- ✗Принудительно вызывать
CollectGarbage()вместо исправления пропущенной ссылки
Уточняющие вопросы
- →Как
gc.PendingKillEnabledили stomp-аллокатор помогли бы локализовать падение? - →Почему добавление
UPROPERTY()превращает падение в безопасное разыменование null, которое можно защитить?
SeniorТеорияИногдаКакова стоимость сборки мусора и как она влияет на время кадра?
Какова стоимость сборки мусора и как она влияет на время кадра?
Проход GC обходит весь достижимый граф объектов, поэтому стоимость растёт с числом UObject. Большой граф может вызвать заметный рывок кадра. UE смягчает это инкрементальным и кластерным GC и настраиваемыми cvar тайминга.
Типичные ошибки
- ✗Думать, что стоимость GC постоянна, а не растёт с числом
UObject - ✗Считать, что GC целиком работает вне игрового потока и никогда не вызывает рывков
- ✗Полагать, что проход, ничего не освободивший, бесплатен
Уточняющие вопросы
- →Как кластеризация сокращает число объектов, которые GC должен трассировать по отдельности?
- →Какие проектные решения держат число
UObjectнизким в большом открытом мире?