/javascriptcore-garbage-collector
JSC GC reference for Bun. Use for use-after-free, JS object leaks, "collected too early", or when touching WriteBarrier, visitChildren, visitAdditionalChildren, JSRef, JSC::Strong/Weak, hasPendingActivity, ensureStillAlive, addOpaqueRoot, reportExtraMemoryAllocated, IsoSubspace,
$ npx -y skills add oven-sh/bun --skill javascriptcore-garbage-collector --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/javascriptcore-garbage-collector
Context preview
The summary Claude sees to decide when to auto-load this skill.
JSC GC reference for Bun. Use for use-after-free, JS object leaks, "collected too early", or when touching WriteBarrier, visitChildren, visitAdditionalChildren, JSRef, JSC::Strong/Weak, hasPendingActivity, ensureStillAlive, addOpaqueRoot, reportExtraMemoryAllocated, IsoSubspace,
SKILL.md
javascriptcore-garbage-collector.SKILL.mdname: javascriptcore-garbage-collector
description: JSC GC reference for Bun. Use for use-after-free, JS object leaks, "collected too early", or when touching WriteBarrier, visitChildren, visitAdditionalChildren, JSRef, JSC::Strong/Weak, hasPendingActivity, ensureStillAlive, addOpaqueRoot, reportExtraMemoryAllocated, IsoSubspace, HeapAnalyzer, finalize.
JavaScriptCore's Garbage Collector (Riptide)
Riptide is **non-moving, generational, parallel, mostly-concurrent, conservative-on-the-stack**. Understanding those five words prevents most GC bugs in Bun.
The mental model
The heap is a graph. GC does a breadth-first search from **roots** → marks everything it reaches → everything unmarked is freed (lazily, on next allocation from that block). It does NOT compact or move objects — pointers stay stable for an object's lifetime.
**Two collection modes:**
- **Eden GC**: only scans newly-allocated objects + remembered set. Fast, frequent.
- **Full GC**: scans everything. Slower, rarer.
**It runs concurrently.** Marking happens on background threads _while JS is executing_; the mutator only stops at brief safepoints. `visitChildren` runs **off the main thread, racing with your code**.
How the VM gathers roots
Roots are not a hardcoded list — they are **marking constraints** registered with `Heap::addMarkingConstraint()` and run to fixpoint. The built-in set lives in `Heap::addCoreConstraints()` (`vendor/WebKit/Source/JavaScriptCore/heap/Heap.cpp:2970`):
| Tag | Name | What it marks | | ----- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Cs` | Conservative Scan | Native stack + registers of every JS thread, scanned word-by-word (`gatherStackRoots` → `ConservativeRoots`). Also JIT stub routines. World is stopped for this. | | `Msr` | Misc Small Roots | `vm.smallStrings`, `m_protectedValues` (`JSValueProtect`/`gcProtect`), `MarkedArgumentBuffer` lists, `vm.exception()` / `lastException()` / `m_terminationException` | | `Sh` | Strong Handles | `m_handleSet.visitStrongHandles()` — every `JSC::Strong<T>`. Also `vm().visitAggregate()` (atom string tables etc.) | | `D` | Debugger | Sampling profiler, type profiler, ShadowChicken | | `Ws` | Weak Sets | Iterates every `WeakBlock`; calls `WeakHandleOwner::isReachableFromOpaqueRoots()` to decide whether a weak ref should _become_ strong this cycle | | `O` | Output | Calls `visitOutputConstraints()` on already-marked cells in output-constraint subspaces (executables, WeakMaps). This is the "re-run after marking discovers more" hook | | `Jw` | JIT Worklist | CodeBlocks queued for compilation | | `Cb` | CodeBlocks | Executing/compiling CodeBlocks |
Bun registers an additional constraint, `DOMGCOutputConstraint` (`src/jsc/bindings/BunGCOutputConstraint.cpp`), which calls `visitOutputConstraints` on every marked cell in Bun's output-constraint subspaces (event targets, generated classes with `visitAdditionalChildren`, etc.).
**Constraint volatility** controls when they re-run during the fixpoint:
- `GreyedByExecution` — may produce new grey cells whenever the mutator runs (re-run after every resume)
- `GreyedByMarking` — may produce new grey cells when _other_ marking happens (re-run after each drain)
- `SeldomGreyed` — usually doesn't add anything; run last
Object layout: the 8-byte JSCell header
Every GC-managed object inherits `JSCell` (`runtime/JSCell.h`):
| StructureID (4) | indexingTypeAndMisc (1) | JSType (1) | flags (1) | cellState (1) |
- `StructureID` — compressed hidden-class pointer
- `indexingTypeAndMisc` — 2 bits are an embedded `WTF::Lock` (the **cell lock**); always CAS this byte
- `cellState` — inlined GC color, used by the write barrier
Out-of-line, in the `MarkedBlock` footer (or `PreciseAllocation` header for objects >~8KB):
- `isMarked` bit — survived last GC
- `isNewlyAllocated` bit — allocated since last GC
Liveness = `isMarked || isNewlyAllocated` (with logical-versioning so blocks aren't swept eagerly).
CellState and the write barrier
`vendor/WebKit/Source/JavaScriptCore/heap/CellState.h`:
PossiblyBlack = 0 // visited (or old-space-pending-rescan during full GC)
DefinitelyWhite = 1 // new / unmarked
PossiblyGrey = 2 // on the mark stack
Generational + concurrent GC share **one** retreating-wavefront barrier:
// After: obj->field = newValue
if (obj->cellState <= blackThreshold) // 0 normally, bumped while GC is marking
writeBarrierSlowPath(obj); // → put obj on remembered set / revisit**You almost never write this by hand.** Use `WriteBarrier<T>` as the field type and call `.set(vm, owner, value)` — it stores then barriers. A raw `JSCell*` / `JSValue` member without a `WriteBarrier` wrapper is a bug: eden GC will free the target out from under you.
`LazyProperty<Owner, T>`, `LazyClassStructure`, and `WriteBarrierStructureID` are barrier-aware variants for lazily-initialized fields and structures.
Allocation: where objects live
`bmalloc/libpas` provides pages; JSC carves them up:
- **`MarkedBlock`** — 16KB block, fixed cell size (segregated free list). Footer holds bitvectors. 16-byte minimum cell al
Read more
name: javascriptcore-garbage-collector description: JSC GC reference for Bun. Use for use-after-free, JS object leaks, "collected too early", or when touching WriteBarrier, visitChildren, visitAdditionalChildren, JSRef, JSC::Strong/Weak, hasPendingActivity, ensureStillAlive, addOpaqueRoot, reportExtraMemoryAllocated, IsoSubspace, HeapAnalyzer, finalize.
JavaScriptCore's Garbage Collector (Riptide)
Riptide is **non-moving, generational, parallel, mostly-concurrent, conservative-on-the-stack**. Understanding those five words prevents most GC bugs in Bun.
The mental model
The heap is a graph. GC does a breadth-first search from **roots** → marks everything it reaches → everything unmarked is freed (lazily, on next allocation from that block). It does NOT compact or move objects — pointers stay stable for an object's lifetime.
**Two collection modes:**
- **Eden GC**: only scans newly-allocated objects + remembered set. Fast, frequent.
- **Full GC**: scans everything. Slower, rarer.
**It runs concurrently.** Marking happens on background threads _while JS is executing_; the mutator only stops at brief safepoints. `visitChildren` runs **off the main thread, racing with your code**.
How the VM gathers roots
Roots are not a hardcoded list — they are **marking constraints** registered with `Heap::addMarkingConstraint()` and run to fixpoint. The built-in set lives in `Heap::addCoreConstraints()` (`vendor/WebKit/Source/JavaScriptCore/heap/Heap.cpp:2970`):
| Tag | Name | What it marks | | ----- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Cs` | Conservative Scan | Native stack + registers of every JS thread, scanned word-by-word (`gatherStackRoots` → `ConservativeRoots`). Also JIT stub routines. World is stopped for this. | | `Msr` | Misc Small Roots | `vm.smallStrings`, `m_protectedValues` (`JSValueProtect`/`gcProtect`), `MarkedArgumentBuffer` lists, `vm.exception()` / `lastException()` / `m_terminationException` | | `Sh` | Strong Handles | `m_handleSet.visitStrongHandles()` — every `JSC::Strong<T>`. Also `vm().visitAggregate()` (atom string tables etc.) | | `D` | Debugger | Sampling profiler, type profiler, ShadowChicken | | `Ws` | Weak Sets | Iterates every `WeakBlock`; calls `WeakHandleOwner::isReachableFromOpaqueRoots()` to decide whether a weak ref should _become_ strong this cycle | | `O` | Output | Calls `visitOutputConstraints()` on already-marked cells in output-constraint subspaces (executables, WeakMaps). This is the "re-run after marking discovers more" hook | | `Jw` | JIT Worklist | CodeBlocks queued for compilation | | `Cb` | CodeBlocks | Executing/compiling CodeBlocks |
Bun registers an additional constraint, `DOMGCOutputConstraint` (`src/jsc/bindings/BunGCOutputConstraint.cpp`), which calls `visitOutputConstraints` on every marked cell in Bun's output-constraint subspaces (event targets, generated classes with `visitAdditionalChildren`, etc.).
**Constraint volatility** controls when they re-run during the fixpoint:
- `GreyedByExecution` — may produce new grey cells whenever the mutator runs (re-run after every resume)
- `GreyedByMarking` — may produce new grey cells when _other_ marking happens (re-run after each drain)
- `SeldomGreyed` — usually doesn't add anything; run last
Object layout: the 8-byte JSCell header
Every GC-managed object inherits `JSCell` (`runtime/JSCell.h`):
| StructureID (4) | indexingTypeAndMisc (1) | JSType (1) | flags (1) | cellState (1) |
- `StructureID` — compressed hidden-class pointer
- `indexingTypeAndMisc` — 2 bits are an embedded `WTF::Lock` (the **cell lock**); always CAS this byte
- `cellState` — inlined GC color, used by the write barrier
Out-of-line, in the `MarkedBlock` footer (or `PreciseAllocation` header for objects >~8KB):
- `isMarked` bit — survived last GC
- `isNewlyAllocated` bit — allocated since last GC
Liveness = `isMarked || isNewlyAllocated` (with logical-versioning so blocks aren't swept eagerly).
CellState and the write barrier
`vendor/WebKit/Source/JavaScriptCore/heap/CellState.h`:
PossiblyBlack = 0 // visited (or old-space-pending-rescan during full GC) DefinitelyWhite = 1 // new / unmarked PossiblyGrey = 2 // on the mark stack
Generational + concurrent GC share **one** retreating-wavefront barrier:
// After: obj->field = newValue
if (obj->cellState <= blackThreshold) // 0 normally, bumped while GC is marking
writeBarrierSlowPath(obj); // → put obj on remembered set / revisit**You almost never write this by hand.** Use `WriteBarrier<T>` as the field type and call `.set(vm, owner, value)` — it stores then barriers. A raw `JSCell*` / `JSValue` member without a `WriteBarrier` wrapper is a bug: eden GC will free the target out from under you.
`LazyProperty<Owner, T>`, `LazyClassStructure`, and `WriteBarrierStructureID` are barrier-aware variants for lazily-initialized fields and structures.
Allocation: where objects live
`bmalloc/libpas` provides pages; JSC carves them up:
- **`MarkedBlock`** — 16KB block, fixed cell size (segregated free list). Footer holds bitvectors. 16-byte minimum cell al
Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
Repo: oven-sh/bun
Other skills on bun.
- /implementing-jsc-classes-cpp
Implements JavaScript classes in C++ using JavaScriptCore. Use when creating new JS classes with C++ bindings, prototypes, or constructors.
Open skill - /implementing-jsc-classes-rust
Creates JavaScript classes using Bun's Rust bindings generator (.classes.ts). Use when implementing new JS APIs in Rust with JSC integration, prototypes, or constructors.
Open skill - /rust-system-calls
Guides using bun_sys for system calls and file I/O in Rust. Use when implementing file operations, opening fds, or any syscall path instead of std::fs or libc.
Open skill - /slowest-tests
Find the top-N slowest test files in CI from a recent BuildKite run, optionally posting the results to a Slack channel as a formatted table. Use when asked to find slow CI tests, "what's making CI slow", or to post a slow-test report to Slack.
Open skill - /verify
Verify a Bun runtime change by driving the debug binary end-to-end.
Open skill - /writing-bundler-tests
Guides writing bundler tests using itBundled/expectBundled in test/bundler/. Use when creating or modifying bundler, transpiler, or code transformation tests.
Open skill

