Что такое корни (roots) в сборщике мусора и где они находятся
Что проверяют
Знаете ли вы, что в .NET считается корнем и как это связано с моментом, когда объект перестаёт быть живым. Вопрос отделяет тех, кто читал про поколения, от тех, кто понимает, почему финализатор способен сработать раньше, чем завершится метод самого объекта.
Как отвечать
Сборщик строит граф живых объектов от набора корней. В .NET это ссылки в стеке и в регистрах процессора у всех выполняющихся потоков, статические поля, дескрипторы сборщика (включая закреплённые и те, что использует взаимодействие с нативным кодом), а также очередь финализации: объект, ждущий финализатора, продлевает жизнь и себе, и всему, на что ссылается.
Дальше стоит сказать, что среда знает время жизни локальных переменных точнее, чем исходный код. JIT-компилятор строит таблицы, по которым переменная перестаёт быть корнем сразу после последнего использования, а не в конце метода. В выпускной сборке это значит, что объект может быть собран, пока его метод ещё выполняется, если дальше нужны только поле или дескриптор.
Слабая ссылка корнем не является: WeakReference и ConditionalWeakTable существуют ровно затем, чтобы кэш или таблица связанных данных не превращались в удержание.
Отсюда практика: нативные дескрипторы держат в SafeHandle, а не в числовом поле, и применяют GC.KeepAlive, когда объект должен дожить до конца вызова в нативный код.
Где спотыкаются
Называют корнями коллекции и кэши. Коллекция не корень, а обычный объект в куче, удерживает её статическое поле или живая переменная, и искать надо именно эту ссылку.
Уверены, что локальная переменная удерживает объект до закрывающей скобки метода. В отладочной сборке так и есть, в выпускной жизнь короче, что и порождает загадочные срабатывания финализатора посреди работы.
Считают, что Dispose освобождает память. Он освобождает неуправляемые ресурсы и снимает объект с очереди финализации, а память вернёт только сборщик и только когда объект станет недостижим от корней.