jam is a compacting generational garbage collector for C++26. Build lists, trees or graphs, keep roots, and let the collector reclaim what you can no longer reach. Sharing and cycles are fine. A pointer takes four bytes, and objects need no GC header or intrusive base class.
Tell us where your pointers live and we generate the walk:
#include <cassert>
#include <cstdint>
#include <new>
import jam.unqualified;
struct node {
ptr<node> next;
std::uint64_t data;
static constexpr auto manifest = make_manifest<node>(&node::next);
};
int main() {
heap heap{{.workers = 4}};
heap_scope scope{heap};
auto const a = mk<node>(nullptr, 42u);
auto const b = mk<node>(a, 99u);
a->next = b;
root answer = a;
collect();
assert(answer->data == 42);
assert(answer->next->next == answer.get());
}Here next is a managed edge; data is just data. The two nodes form a cycle.
answer keeps it alive, and collection repairs the links as objects move.
Drop the root and the whole cycle becomes collectible.
Manifests compose through embedded records and arrays. You can store SIMD
pointer vectors too, or supply a trace hook when the layout depends on a tag.
The tracing guide and pointer vector examples
pick up from there. A weak_ptr<T> follows movement without keeping its target
alive. A weak_root<T> does the same from outside
the heap and stays valid across collection.
Weak associations and finalizers let a live key keep a value alive and schedule a managed cleanup action when the key dies.
There are a few things you have to get right:
- List every managed edge in a manifest or trace hook. Unlisted fields are data; Jam cannot discover a pointer you haven't told it about.
- Keep roots outside the heap and pointers inside it. The local
aandbabove are stale aftercollect(); get fresh pointers fromanswer. Raw pointers and references into the heap can also expire when allocation grows it. - Use the owning
heap_scopewhen working with pointers or roots, including when destroying roots. Mixing heaps is undefined behavior. Collection can use several workers, but you must stop mutating the heap before collecting. - Objects move as bytes. Jam does not call their move constructors or run their
destructors when reclaiming them. Self-links through
ptrwork; raw self-pointers don't. Cleanup needs an explicit finalizer action, not a C++ destructor. See the storage contract before choosing what to put in the heap.
You choose when to call collect(). Most calls collect only young objects;
periodically one collects both generations. Writes from old objects to young
ones are tracked automatically. Collection policy and sizing
cover the controls when you need them.
To try it, you'll need Clang 23+, CMake 4.4+ and Ninja on macOS, Linux or Windows.
The build guide has the commands. import jam.unqualified;
puts the names in scope as above; use import jam; for qualified names.
The topic guides go deeper; the API reference documents the individual operations.
Jam also backs HotSpot, GraalVM and Native Image. The runtime integration lives here too; the default build is still just the C++ library.
See LICENSE.md for the dual BSD-2-Clause/Apache-2.0 license and individual source notices for retained upstream terms.
Contributions and bug reports are welcome through GitHub.
Edward Kmett can also be reached as ekmett on Libera Chat and @kmett on Twitter/X.
