Definition and Overview

Garbage collection (GC) on the BEAM virtual machine is a per-process, generational, copying collector that automatically reclaims memory no longer reachable by a process. Unlike garbage collectors in the JVM, .NET, or Go -- which operate on shared heaps and cause global application pauses -- BEAM's GC is scoped to individual processes. When a process's garbage collector runs, only that process is paused; all other processes continue executing without interruption. This per-process isolation is the architectural foundation of BEAM's soft real-time guarantees.

The generational hypothesis (most objects die young) guides BEAM's GC strategy. Each process maintains two heap generations: a young generation (new heap) where recently allocated data lives, and an old generation (old heap) where data that has survived multiple collections is promoted. Minor collections scan only the young generation, which is fast because most data dies quickly. Major collections (full sweeps) scan both generations, reclaiming long-lived data that has become unreachable.

The per-process GC model has profound implications for application design. A process with a 2KB heap triggers microsecond GC pauses. A process accumulating megabytes of state triggers longer pauses that only affect that process. This means BEAM applications achieve consistent latency by distributing state across many small processes rather than concentrating it in a few large ones. Understanding GC behavior is essential for building Elixir applications that meet the Prismatic Platform's sub-250ms page load requirement.

Technical Deep Dive

GC Algorithm Phases

PhaseScopeTriggerDuration
Minor GCYoung heap onlyYoung heap full~10-100 us
Major GCYoung + old heapOld heap full or fullsweep_after reached~100 us - 10 ms
Heap growthNeitherCurrent heap too smallHeap resized (doubles)
Heap shrinkNeitherHeap utilization < 25% after GCHeap compacted

Generational Collection Flow

Allocation:
  New data -> Young Heap (new_heap)

Minor GC (when young heap full):
  1. Scan roots (stack, registers, mailbox)
  2. Copy live young data to new young heap
  3. Promote data surviving N minor GCs to old heap
  4. Reclaim old young heap entirely

Major GC (fullsweep, when old heap full):
  1. Scan all roots
  2. Copy all live data (young + old) to new single heap
  3. Re-split into young and old generations
  4. Reclaim all old heaps

Key GC Parameters

ParameterDefaultEffectTuning Guidance
fullsweep_after65535Minor GCs before forced fullsweepLower for memory-constrained; 0 for always fullsweep
min_heap_size233 wordsInitial process heap sizeIncrease for processes that immediately allocate
min_bin_vheap_size46422 wordsVirtual binary heap sizeIncrease for binary-heavy processes
max_heap_size0 (unlimited)Kill process if exceededSet to prevent runaway memory
message_queue_dataon_heapWhere messages are storedoff_heap for mailbox-heavy processes

BEAM GC vs Other Runtimes

PropertyBEAMJVM (G1GC)Go.NET
ScopePer-processGlobal heapGlobal heapPer-generation
Pause visibilitySingle processAll threadsAll goroutinesAll threads
Max pause (P99)~1ms per process10-200ms0.5-5ms10-100ms
Global pauseNeverYes (though concurrent)Yes (STW phases)Yes (compaction)
GenerationalYes (2 gen)Yes (young/mixed/old)NoYes (3 gen)
CompactingYes (copying)YesNo (non-moving)Yes
ConcurrentNo (per-process stop)Mostly concurrentMostly concurrentMostly concurrent

Architecture and Implementation

BEAM's GC architecture is inseparable from its process model. Each process is allocated an initial heap (typically 233 words, ~1.8KB on 64-bit) that grows dynamically as the process allocates data. The heap consists of contiguous memory regions for the young and old generations, plus a separate area for large binaries (the binary virtual heap).

When a process allocates data and the current heap is full, a minor GC is triggered. The collector traces from the process's root set (stack frames, registers, mailbox references) through all reachable data, copying live objects to a new heap area. Because BEAM data is immutable, there are no write barriers or remembered sets needed for generational promotion -- any pointer from old generation to young generation was established at allocation time and is tracked by the promotion count.

Binary data larger than 64 bytes is allocated on a shared binary heap and reference-counted. These off-heap binaries are not subject to generational collection; instead, their reference counts are decremented during GC, and the binary is freed when the count reaches zero. This is why binary data can sometimes accumulate in BEAM applications -- if a process holds a reference to a large binary but does not trigger GC, the binary remains allocated.

Usage in Prismatic Platform

The Prismatic Platform configures GC parameters for different process profiles to optimize latency and memory usage.

defmodule Prismatic.GC.Tuning do
  @moduledoc """
  GC tuning utilities for different process profiles.
  Configures BEAM garbage collection parameters to
  meet the platform's performance requirements.
  """

  @type profile :: :low_latency | :high_throughput | :memory_constrained | :long_lived

  @spec apply_profile(pid(), profile()) :: :ok
  def apply_profile(pid, :low_latency) do
    Process.flag(pid, :fullsweep_after, 10)
    Process.flag(pid, :min_heap_size, 1000)
    :ok
  end

  def apply_profile(pid, :high_throughput) do
    Process.flag(pid, :fullsweep_after, 65535)
    Process.flag(pid, :min_heap_size, 10_000)
    :ok
  end

  def apply_profile(pid, :memory_constrained) do
    Process.flag(pid, :fullsweep_after, 0)
    Process.flag(pid, :max_heap_size, %{size: 1_000_000, kill: true, error_logger: true})
    :ok
  end

  def apply_profile(pid, :long_lived) do
    Process.flag(pid, :fullsweep_after, 100)
    Process.flag(pid, :message_queue_data, :off_heap)
    :ok
  end

  @spec collect_gc_stats(pid()) :: map()
  def collect_gc_stats(pid) do
    {:garbage_collection, gc_info} = Process.info(pid, :garbage_collection)
    {:memory, memory} = Process.info(pid, :memory)
    {:message_queue_len, queue_len} = Process.info(pid, :message_queue_len)

    %{
      heap_size: Keyword.get(gc_info, :heap_size, 0),
      old_heap_size: Keyword.get(gc_info, :old_heap_size, 0),
      fullsweep_after: Keyword.get(gc_info, :fullsweep_after, 0),
      minor_gcs: Keyword.get(gc_info, :minor_gcs, 0),
      memory_bytes: memory,
      message_queue: queue_len
    }
  end
end

LiveView processes use the :low_latency profile to ensure consistent render times. Agent GenServer processes use :long_lived with off-heap message queues to avoid GC pressure from high message throughput. The OSINT tool execution processes use :high_throughput to minimize GC interruptions during data processing.

Cross-References

  • Heap -- Process memory allocation area
  • ETS Memory -- Memory management outside GC scope
  • Process Isolation -- Isolation enabling per-process GC
  • BEAM -- Virtual machine implementing the GC
  • Execution Time -- GC impact on operation timing
  • Livebooks: performance_monitoring/ notebooks include GC profiling tools
  • Academy: Topics on BEAM internals cover GC mechanics

Connect & Contribute

Created by Tomas Korcak (korczis) | Open Source under GHL