Ответ 1
Я не знаю, нашли ли вы какое-то исключительно плохое описание или ожидаете ли вы слишком много деталей, я был вполне доволен объяснением видел. Если описания краткие и упрощенные, это потому, что это довольно простой механизм.
Как вы, по-видимому, уже знаете, сборщик мусора поколения должен иметь возможность перечислять старые объекты, относящиеся к молодым объектам. Было бы правильно сканировать все старые объекты, но это разрушает преимущества подхода поколений, поэтому вам нужно сузить его. Независимо от того, как вы это делаете, вам нужен барьер записи - кусок кода, выполняемый всякий раз, когда к переменной-члену (ссылочного типа) назначается/записывается. Если новая ссылка указывает на молодой объект и хранится в старом объекте, барьер записи записывает этот факт для сбора мусора. Разница заключается в том, как она записана. Существуют точные схемы с использованием так называемых запоминаемых множеств, совокупность каждого старого объекта, который (имел в какой-то момент) ссылку на молодой объект. Как вы можете себе представить, это занимает довольно много места.
Карточный стол - это компромисс: вместо того, чтобы сообщать вам, какие объекты точно содержат молодые указатели (или, по крайней мере, в какой-то момент), он группирует объекты в ведра и треки фиксированного размера, в которых ведра содержат объекты с молодыми указателями. Это, конечно, уменьшает использование пространства. Для правильности не имеет значения, как вы ведете объекты, если вы согласны в этом. Для эффективности вы просто группируете их по их адресу памяти (потому что у вас есть это бесплатно), деленная на более большую мощность в два (чтобы сделать деление дешевой побитовой операции).
Кроме того, вместо сохранения явного списка ведер, вы резервируете некоторое пространство для каждого возможного ведра вверх. В частности, существует массив из N бит или байтов, где N - количество ведер, так что значение i
th равно 0, если в ведре i
th нет молодых указателей или 1, если он содержит молодые указатели, Это собственно карточный стол. Обычно это пространство выделяется и освобождается вместе с большим блоком памяти, используемым в качестве (части) кучи. Он даже может быть встроен в начало блока памяти, если ему не нужно расти. Если все адресное пространство используется как куча (что очень редко), приведенная выше формула дает числа, начинающиеся с start_of_memory_region >> K
вместо 0, поэтому, чтобы получить индекс в карточном столе, вы должны вычесть начало начального адреса куча.
Таким образом, когда барьер записи обнаруживает, что оператор some_obj.field = other_obj;
хранит молодой указатель в старом объекте, он делает это:
card_table[(&old_obj - start_of_heap) >> K] = 1;
Где &old_obj
- адрес объекта, у которого теперь есть молодой указатель (который уже находится в регистре, потому что он просто решил ссылаться на старый объект).
Во время незначительного GC сборщик мусора смотрит на карточный стол, чтобы определить, какие области кучи сканируются для молодых указателей:
for i from 0 to (heap_size >> K):
if card_table[i]:
scan heap[i << K .. (i + 1) << K] for young pointers