Позвольте мне дать вам краткое введение в трехцветный GC (если кто-то прочитает его, который никогда не слышал об этом); если вам все равно, пропустите его и перейдите к проблеме.
Как работает Tri-Color GC
В трехцветном GC объект имеет один из трех возможных цветов; белый, серый и черный. Трехцветный GC можно описать следующим образом:
-
Все объекты изначально белые.
-
Все объекты достижимы, потому что глобальная переменная или переменная стека ссылаются на нее ( "корневые объекты" ) окрашены в серый цвет.
-
Мы берем любой серый объект, находим все ссылки на белые объекты и окрашиваем эти белые объекты в серый цвет. Затем мы окрашиваем сам объект в черный цвет.
-
Мы продолжаем на шаге 3, пока у нас есть серые объекты.
-
Если у нас нет серых объектов, все остальные объекты либо белые, либо черные.
-
Все черные объекты, как оказалось, были доступны и должны оставаться в живых. Все белые объекты недоступны и могут быть удалены.
Пока это не слишком сложно... по крайней мере, если GC - это StW (Stop the World), то есть он приостанавливает все потоки при сборе мусора. Если он является параллельным, трехцветный GC имеет инвариант, который должен всегда оставаться истинным:
Черный объект не должен ссылаться на белый объект!
Это выполняется автоматически для StW GC, так как каждый объект, который окрашен в черный цвет, был рассмотрен ранее, и все белые объекты, на которые он указывал, были окрашены в серый цвет, поэтому черный объект может ссылаться только на другие черные объекты или серые объекты.
Если потоки не приостановлены, потоки могут выполнять код, который нарушит этот инвариант. Существует несколько способов предотвратить это:
-
Захватите весь доступ для чтения к указателям и посмотрите, доступен ли этот доступ для чтения к белому объекту. Если это так, немедленно окрасьте этот объект в серый цвет. Если ссылка на этот объект теперь назначена черному объекту, это не имеет значения, объект будет серым, а не белым дольше (эта реализация использует барьер чтения)
-
Захватите весь доступ к записи указателям и посмотрите, белый ли присвоенный объект, а назначенный ему объект - черный. Если это так, окрасьте белый объект в серый цвет. Это более очевидный способ делать вещи, но также требует немного большего времени обработки (эта реализация использует барьер записи)
Поскольку обращения к чтению гораздо более распространены, чем записи с доступом к записи, хотя вторая возможность включает в себя большее время обработки при ударе барьера, она называется реже и такая привилегированная. GC, работающий подобным образом, называется "инкрементным обновлением GC".
Существует альтернатива обеим методам, называемая SatB (Снимок в начале). Этот вариант работает несколько иначе, учитывая тот факт, что на самом деле нет необходимости постоянно поддерживать инвариант, поскольку не имеет значения, относится ли черный объект к белому, если GC знает, что этот белый объект раньше был и все еще доступен во время текущего цикла GC (либо потому, что все еще есть серые объекты, ссылающиеся на этот белый объект, либо потому, что ссылка ref на этот белый объект помещается в явный стек, который также рассматривается GC при его запуске из серых объектов). Сборщики SatB чаще используются на практике, потому что они имеют некоторые преимущества, но имхо их сложнее реализовать.
Я имею в виду здесь инкрементное обновление GC, которое использует вариант 2: всякий раз, когда код пытается сделать черный объект указывать на белый объект, он сразу же окрашивает объект в серый цвет. Таким образом, этот объект не будет пропущен в цикле сбора.
Проблема
Так много о трехцветных GC. Но есть одна вещь, которую я не понимаю о трехцветных GC. Предположим, что у нас есть объект A, на который ссылается стек, и сам относится к объекту B.
stack -> A.ref -> B
Теперь GC запускает цикл, останавливает поток, просматривает стек и видит A как доступный напрямую, окрашивая серый. Как только это будет сделано при сканировании всего стека, он снова отключит поток и начнет обработку на шаге (3). Прежде чем он начнет что-либо делать, он выгружается (может случиться), и поток снова запускается и выполняет следующий код:
localRef = A.ref; // localRef points to B
A.ref = NULL; // Now only the stack points to B
sleep(10000); // Sleep for the whole GC cycle
Поскольку инвариант не был нарушен, B был белым, но не был назначен черному объекту, цвет B не изменился, он все еще белый. A больше не ссылается на B, поэтому при обработке "серого" A B не изменит свой цвет, а A станет черным. В конце цикла B все еще белый и выглядит как мусор. Однако localRef относится к B, поэтому это не мусор.
Вопрос
Правильно ли, что трехцветный GC должен дважды проверять стек каждого потока? Как только в самом начале, чтобы идентифицировать корневые объекты (получая цвет серый) и снова, перед удалением белых объектов, поскольку на них может ссылаться стек, хотя ни один другой объект не ссылается на них больше. Нет описания алгоритма, который я видел до сих пор, упоминал о сканировании стека дважды. Они все только говорили, что при одновременном использовании важно, чтобы инвариант был применен во все времена, иначе достижимые объекты пропущены. Но, насколько я вижу, этого недостаточно. Стек должен рассматриваться как один большой объект, и как только он сканируется, "стек черный", и каждое обновление ref для стека должно вызвать цвет серого объекта.
Если это действительно так, использование инкрементного обновления может быть более сложным, чем я изначально думал и имеет некоторые недостатки в производительности, так как изменения в стеке являются наиболее частыми.