A copying collector splits the heap into two halves and uses only one at a time. Today you build that semispace heap with bump allocation, the layout that makes compaction possible.
Build a two-space heap that bump-allocates objects into the active half.
A copying collector divides the heap into two equal halves, called semispaces: from-space (the active half, where objects live and allocation happens) and to-space (the reserve half, empty and waiting). Only one half is ever in use at a time, which is the price of admission - you trade half your memory for the ability to compact. In return, allocation becomes trivially fast: a bump of a cursor through from-space, exactly like the very first bump allocator, with no free list to consult.
Build the two spaces and allocate by bumping. A Ref is again an index, but now an
index into the active space. Reuse the same object model - fields that hold Refs,
and a root set with the same AddRoot and Roots methods as the mark-sweep heap
(you will need them once collection starts) - so the graph you build looks identical to
the mark-sweep heap’s; only the collection strategy differs. Free reports the unused
slots in the active half.
Bumping fills from-space quickly and never reuses a slot; the whole point of the next
lessons is what happens when it fills - the live objects get copied, compacted, into
the empty to-space.
type CopyingHeap struct {cap int // slots per semispacefrom, to []*object // active space, reserve spacenext int // bump cursor into the active spaceroots map[Ref]bool}func NewCopyingHeap(cap int) *CopyingHeap {return &CopyingHeap{cap: cap, from: make([]*object, cap),to: make([]*object, cap), roots: map[Ref]bool{}}}func (h *CopyingHeap) New(nfields int) (Ref, error) {f := make([]Ref, nfields)for i := range f { f[i] = Nil }r := h.next; h.from[r] = &object{fields: f}; h.next++; return r, nil}func (h *CopyingHeap) Free() int { return h.cap - h.next }