Все сильно зависит от движка, но в основном принципы похожи. Расскажу на примере v8 (chrome, node).
Первое, что надо понять, в v8 сборка мусора основана на поколениях. И в разных поколениях применяются разные алгоритмы. В v8 используется 3 поколения:
- Молодое (собирается по наполнению (часто), но быстрым упрощенным алгоритмом)
- Старое (собирается по расписанию (редко), здесь как раз Mark & Sweep)
- Особое (я не очень про него знаю, здесь очень большие объекты + объекты с прямым доступом из глобального)
Второе, что нужно понять, все данные доступные из JS v8 хранит на куче, независимо примитив это или js-объект. С точки зрения GC все есть объект, и числа и строки и функции.
Теперь более подробно про молодое поколение. Его цель - быть быстрым, быстро выделять и освобождать память. Создатели v8 прекрасно знают, что аллокация на куче - крайне затратная операция, поэтому здесь преаллоцированы 2 страницы памяти, которые используются по очереди. Заодно можно получить бонус в работе с процессорным кэшем, за счет того, что здесь "горячие" данные и они лежат рядом, а значит попадут на одну кэш линию. Когда мы создаем новый объект, v8 просто помещает его в конец активной страницы. Если места не хватает происходит быстрая сборка мусора. А еще здесь используется подсчет ссылок (он быстрее), но только для ссылок из старого поколения - если есть хоть 1 такая ссылка - объект живой, а так же живо все его объектное дерево. Так же нельзя "убивать" объект, если на него есть ссылки из стэка. Все остальное "мертвое". Живые объекты переносятся в начало второй страницы памяти (заодно происходит дефрагментация памяти), после чего она становится активной. Если объект пережил 3 таких быстрых сборки, то вместо второй страницы его переносят на старое поколение, при этом происходит аллокация памяти.
В Вашем примере задействовано только молодое поколение.
function f() {
// это мертвый код, после оптимизации f строка вообще не будет память использовать
let a = 'some text';
// это 2 молодых объекта, на них уже ссылается контекст вызова f, а на него ссылается стэк
var obj1 = {};
var obj2 = {};
obj1.p = obj2; // obj1 references obj2
obj2.p = obj1; // obj2 references obj1. This creates a cycle.
// при завершении функции стэк перестает ссылаться на контекст вызова
// контекст вызова умрет при ближайшей GC,
// а вместе с ним и obj1 и obj2, так как их никто не отметит "живыми"
}
f();