Skip to content
Development
Skill

/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,

From plugin
bun
95k8 skills6 commands
Install
$ npx -y skills add oven-sh/bun --skill javascriptcore-garbage-collector --agent claude-code

How 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.md
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
Read more
Ships withbun

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one

Get the whole plugin