Skip to content

Garbage Collector

StarLang uses a mark-and-sweep garbage collector for automatic memory management.

Managed Types

The GC tracks heap-allocated objects:

Type Allocation
Array StarArray struct + items buffer
Dict StarDict struct + keys + values buffers
Struct StarStruct struct + fields buffer
Channel StarChannel struct + ring buffer + pending send values
Result StarResult struct + inner value

Strings from the constant pool are not GC-managed — they live in the bytecode buffer.

Object Tracking

Every heap object has a header:

typedef struct StarObj {
    struct StarObj *next;   // linked list
    StarObjType    type;    // OBJ_ARRAY, OBJ_DICT, OBJ_STRUCT, OBJ_CHANNEL, or OBJ_RESULT
    bool           marked;  // GC mark bit
} StarObj;

All objects are linked into a single list (vm->objects). New objects are prepended to the head.

Mark Phase

The collector uses an iterative worklist algorithm to mark reachable objects. This avoids C stack overflow on deeply nested structures — critical for ESP32 where the call stack is small.

Worklist capacity is 256 entries (stack-allocated, no heap cost). The algorithm:

  1. Scan all GC roots, enqueue unmarked arrays/dicts into the worklist
  2. Pop objects from the worklist, scan their contents, enqueue any unmarked children
  3. Repeat until the worklist is empty

If the worklist overflows (>256 objects in flight), the collector sets an overflow flag and runs a follow-up pass: it walks the full object list, re-scanning marked objects for unmarked children. This repeats until no new objects are discovered. Correctness is guaranteed regardless of nesting depth — overflow only costs an extra linear scan.

GC roots:

  • Stackstack[0..sp-1]
  • Task stacks — all task stacks tasks[0..task_count-1].stack[0..sp-1] (when multitasking is active)
  • Globalsglobals[0..global_count-1]
  • Constantsconstants[0..const_count-1]

Channel objects get special treatment during marking: the collector scans the ring buffer (buffer[0..len-1]) and the pending send values (send_values[0..send_waiter_count-1]) for reachable objects.

Result objects are scanned for their .value field, which may reference a heap-allocated object (string, array, etc.).

Sweep Phase

The collector walks the object list. Unmarked objects are freed. Marked objects have their mark bit reset for the next cycle.

Stop-the-World

GC runs stop-the-world — all execution pauses during collection. On PC this is negligible. On ESP32, use gc.collect() before time-critical sections to control when the pause happens:

gc.collect()
while (true) {
    readSensor()
}

Trigger

  • GC runs automatically when bytes_allocated >= next_gc
  • After each collection: next_gc = bytes_allocated * grow_factor
  • On platforms with limited RAM, max_gc caps the threshold to prevent runaway growth

Platform Defaults

Platform Initial Threshold Grow Factor Max Threshold
PC 1 MB 2.0x — (no cap)
ESP32 32 KB 1.5x 128 KB

Without max_gc, the threshold grows unbounded:

32 KB → 48 KB → 72 KB → 108 KB → 162 KB  ← OOM on ESP32

With max_gc = 128 KB, GC triggers more frequently instead of running out of memory.

Configuration

The HAL sets platform-appropriate values at startup:

// PC defaults (set by star_vm_init)
vm->next_gc = 1024 * 1024;    // 1 MB
vm->gc_grow_factor = 2.0;
vm->max_gc = 0;                // no cap

// ESP32 (set by HAL)
star_vm_configure_gc(vm, 32 * 1024, 1.5, 128 * 1024);

Manual Collection

Use gc.collect() to trigger a collection manually:

gc.collect()

Currently returns void. A future version may return the number of bytes freed.

Shutdown

star_vm_free() frees all remaining objects when the VM shuts down, regardless of reachability.