Memory Models — Cross-Language Comparison

Memory Models — Cross-Language Comparison

  • Type: cross-cutting comparison
  • Languages covered: 51 (see _index)
  • Last updated: 2026-05-07

TL;DR

  • Five families dominate: manual, RAII + ownership, reference counting, tracing GC, and image/region-based. Most modern languages are hybrids.
  • The biggest practical axis is who pauses your program: tracing GCs do (in microseconds-to-seconds), refcounted languages don’t (but have cycle-collection costs and atomic-decrement overhead in MT). Ownership systems push the cost to compile time.
  • “GC” hides huge variation: V8’s incremental scavenge is nothing like Java ZGC’s pauseless or Erlang’s per-process heaps. Treat the family as a starting point, not a verdict.
  • Several recent languages (Lean 4 FBIP, Roc uniqueness, Pony reference capabilities) sit between RC and ownership — they get RC’s predictability with ownership’s no-pause guarantee, by typing uniqueness.
  • Image-based (Smalltalk/Pharo) is its own thing: the “heap” is a persistent file you snapshot and resume. There is no boundary between disk and memory.

The spectrum

manual  RAII   ownership  refcaps   RC     hybrid-RC+cycles   tracing-GC   image-based
  |      |        |         |        |             |               |            |
  C    C++     Rust       Pony    Swift     Python/Perl/PHP    Java/Go/JS    Pharo
  Zig  Crystal           Idris2  ObjC ARC                      C#/Erlang    Squeak
  Odin lib::                               Lean4-FBIP          Ruby/Lua     Common Lisp
                                           Roc-uniq            Elixir/BEAM

Categories

1. Manual / explicit

You call malloc/free (or equivalent). The compiler does not insert deallocations. Maximum control, maximum footgun surface.

  • Cmalloc/free, no destructors. UB on use-after-free, double-free, leak. Sanitizers (ASan/UBSan) catch most at runtime.
  • C++ — manual is available (new/delete) but RAII is the idiom; raw pointers exist for interop.
  • Zig — allocators are first-class parameters; you must thread an Allocator through every API. No hidden allocations. Zig’s “all allocations are explicit” is the language’s defining property.
  • Odin — allocator is part of an implicit context struct passed everywhere; you can swap it (arena, temp, GPA) per scope. Closer to “convention-explicit” than fully manual.
  • Fortranallocate/deallocate; arrays can be auto/stack or heap-allocated.

2. RAII + smart pointers

Destructors run when an object goes out of scope. Memory cleanup is tied to control-flow, not GC. Smart pointers (unique_ptr, shared_ptr) provide higher-level disciplines on top.

  • C++ — the canonical RAII language. std::unique_ptr (move-only ownership), std::shared_ptr (refcounted), std::weak_ptr (break cycles). Custom allocators are a major axis.
  • Rust — RAII via Drop, but with compiler-checked exclusive ownership (“affine types”). Values move by default; you opt into refcount with Rc/Arc. See category 3.
  • Ada — controlled types provide RAII; Initialize/Adjust/Finalize.
  • Pascal (Object Pascal/Delphi) — interfaces are refcounted (RAII-flavored); class instances are manual Free.

3. Ownership / borrow checking

Compile-time check that exactly one owner exists at a time, with temporary borrows tracked through lifetimes. No runtime cost. Hard to learn but free at runtime.

  • Rust — the canonical example. Affine types + lifetimes + Send/Sync traits.
  • V — claims ownership-style autofree; in practice still WIP; falls back to GC by default as of v0.5.x.

4. Reference capabilities (typed uniqueness for actor messages)

Like ownership, but the type system tracks 6 capabilities (iso/val/ref/box/tag/trn) so the compiler can prove data-race freedom in actor message-passing. Per-actor GC underneath.

  • Pony — the only mainstream language with this. ORCA distributed GC. Each actor has a private heap; iso types can move between actors zero-copy, val types share immutably.

5. Reference counting (single-threaded or atomic)

Increment on copy, decrement on drop, free at zero. No tracing pause. Cycles leak unless a cycle collector runs. Atomic increment/decrement for shared refs is non-trivial cost (~10x non-atomic).

  • Swift — ARC (Automatic Reference Counting) inserted by compiler. No cycle collector — you manage cycles with weak/unowned.
  • Lean — Lean 4 uses FBIP (functional but in-place): a refcount-based approach that, when the compiler proves a value has refcount 1, mutates in place. Functional code with imperative performance, no tracing GC. See Counting Immutable Beans.
  • Roc — uniqueness/borrow inference informs the compiler when to mutate in place vs copy; refcounted under the hood.
  • Objective-C — ARC since 2011 (mention for completeness; not in our 51).

6. Hybrid: refcount + cycle collector

Refcount handles the common case; a periodic mark-sweep collects cycles. Most “scripting” languages are this.

  • Python — CPython refcounts + generational cycle collector (gc module). The free-threaded build (PEP 779, 3.14) preserves this model, with atomic refcounts.
  • PHP — refcount + cycle GC since PHP 5.3.
  • Perl — refcount; no cycle collector — circular refs leak unless you use Scalar::Util::weaken.

7. Tracing GC — generational + young/old

Mark-and-sweep in regions, with a young generation (most allocations die young) for cheap collection. Standard for managed-runtime languages.

  • Java — multiple collectors: G1 (default), Parallel, Serial; ZGC and Shenandoah are pauseless (see category 8).
  • [[Languages/csharp|C#]] / .NET — server vs workstation GC; concurrent GC variants.
  • Go — concurrent tri-color mark-sweep, very low pause. Not generational by design (compaction-free).
  • JavaScript (V8) — Orinoco: parallel scavenge + concurrent major GC.
  • TypeScript — inherits the JS runtime’s GC.
  • Dart — generational scavenger; Flutter relies on its determinism.
  • Kotlin — JVM GC for JVM; LLVM/native uses its own (Memory Manager, formerly ARC-based, now tracing as of 1.7.20).
  • Scala — JVM GC.
  • Groovy — JVM GC.
  • Clojure — JVM (or BEAM, JS) GC.
  • [[Languages/fsharp|F#]] — .NET GC.
  • Ruby — generational + incremental + compacting (Ruby 3.x).
  • Lua — incremental mark-sweep; 5.4+ adds optional generational mode.
  • R — generational mark-and-sweep.
  • Julia — non-moving mark-sweep, generational. Multi-threaded GC since 1.10.
  • Crystal — Boehm conservative GC. Plans for precise.
  • Racket — incremental, generational.
  • Scheme — implementation-dependent (Chicken uses Cheney-on-the-MTA, Chez uses generational, Guile uses BDW-Boehm).
  • Common Lisp — implementation-dependent (SBCL uses precise generational).
  • Smalltalk (Pharo) — generational GC, plus see image-based below.
  • Groovy — JVM.

8. Tracing GC — pauseless / concurrent

The collector runs concurrently with the mutator and produces pauses well under 1ms even on multi-GB heaps. Trades throughput for predictability.

  • Java — ZGC (sub-ms pauses up to 16TB heaps), Shenandoah.
  • Go — concurrent GC with sub-ms pauses by design (sacrifices throughput for latency).
  • Erlang / Elixir — per-process heap means GC is per-process, not stop-the-world. The whole VM never pauses; one process’s GC pauses only that process.
  • [[Languages/csharp|C#]] — server GC has concurrent compaction.

9. Region-based / arena

Memory is allocated in regions/arenas; a whole region is freed at once. Common in compilers, parsers, request-scoped servers.

  • Nimarc/orc modes provide ownership-style RC + cycle collector. Older modes (refc, markandsweep, boehm, regions, none) still selectable.
  • OCaml — minor heap is essentially a bump-pointer region; major heap is mark-sweep. The minor-heap design is why allocation is so cheap.
  • Erlang / Elixir — each process has its own heap (a region). Process death frees the region in O(1).
  • Haskell — GHC’s nursery is a bump region; promoted to older generation.

10. Image-based / persistent

The “heap” is a file. You start the system by loading the image; you save the world by writing it. There’s no source-vs-runtime distinction; the running objects are the program.

  • Smalltalk (Pharo, Squeak) — the IDE, your app, and the language itself live in one image. Smalltalk save writes the heap to disk.
  • Common Lispsave-lisp-and-die (SBCL), dump-lisp-image (CCL) — same idea. ASDF + Quicklisp manage code; the image is the running state.

11. Linear / quantitative types — academic-leaning

Compile-time tracking of how many times a value is used (0, 1, ω). Lets the compiler reason about resource lifetime statically.

  • Idris (2) — Quantitative Type Theory: every binding has a quantity (0/1/ω). Linear (1-use) values can be safely consumed without GC.
  • Haskell — Linear Haskell extension (-XLinearTypes).
  • Agda — quantity annotations as of 2.6+.
  • Lean — does not have linear types in the Idris sense; FBIP achieves similar runtime behavior via RC analysis.

12. No real heap / stack-allocated only

  • COBOL — no dynamic allocation in the classical language; modern COBOL has POINTER + ALLOCATE/FREE.
  • SQL — N/A; the engine manages memory.
  • Bash / PowerShell — variables are strings/objects; the shell handles memory.
  • Tcl — refcounted strings/lists/dicts under the hood; you don’t see it.

Per-language quick reference

LanguageFamilyMechanismWatch out for
PythonHybrid RC + cycleCPython refcount + generational cycle GCdel + cycles; PEP 703 free-threaded
JavaScriptTracingV8 Orinoco (parallel scavenge + concurrent major)Closure retention; finalization registry
TypeScriptTracing(inherits JS runtime)same as JS
JavaTracingG1 default; ZGC/Shenandoah pauselessAllocation pressure; finalizers (deprecated)
CManualmalloc/freeUB galaxy; use ASan/UBSan
C++RAII + manualunique_ptr/shared_ptr; custom allocatorsCycles in shared_ptr; iterator invalidation
[[Languages/csharp|C#]]Tracingserver/workstation GC; concurrentLOH fragmentation; GC.KeepAlive
GoTracing concurrentconcurrent tri-color mark-sweepGoroutine leaks; finalizers are weak
RustOwnershipaffine types + lifetimes; opt-in Rc/ArcRc<RefCell<T>> cycles still leak
SwiftRCARC; weak/unowned for cyclesStrong reference cycles
KotlinTracingJVM GC; native uses tracing (post-1.7.20)Coroutine retention via captured refs
RubyTracinggenerational + incremental + compactingObjectSpace.each_object cost
PHPHybrid RC + cyclerefcount + cycle GC since 5.3gc_collect_cycles tuning
ScalaTracingJVM GCLaziness retains classloaders
DartTracinggenerational scavengerFlutter freeze frames
ElixirTracing per-processper-BEAM-process heap, copy-on-sendLarge message copies
HaskellTracingGHC nursery + generationalSpace leaks via thunks (! and seq)
ClojureTracinghost runtime (JVM/CLR/JS)Holding head of lazy seq
[[Languages/fsharp|F#]]Tracing.NET GCSame as C#
LuaTracingincremental mark-sweep; 5.4+ generational mode__gc finalizer ordering
RTracinggenerational mark-and-sweepCopy-on-modify can surprise
JuliaTracingnon-moving mark-sweep, generationalAllocation in hot loops
ZigManual + allocatorsexplicit Allocator parameterForgotten defer
NimRC + cycle (ARC/ORC)switchable: arc/orc/refc/boehm/markandsweep/regions/noneUse ORC for cyclic data
CrystalTracingBoehm conservative GCImprecise root scanning
OCamlRegion + tracingminor heap (bump) + major (mark-sweep)Compaction triggers
PerlRCrefcount; NO cycle collectorCycles leak; use Scalar::Util::weaken
ErlangTracing per-processper-process private heapAtom table leaks
RacketTracingincremental generationalCustodian-managed resources
Common LispTracing + imageimpl-specific (SBCL: precise gen); image-saveableGeneration tuning per impl
SchemeTracingimpl-specific (Chez gen, Chicken Cheney-on-MTA, Guile BDW-Boehm)Continuations capture stack snapshots
FortranManualallocate/deallocate + automatic arraysCoarrays distributed across images
COBOLMostly staticclassical = no heap; modern POINTER + ALLOCATEMostly N/A
AdaManual + RAIIcontrolled types; access types with unchecked_deallocationStorage pools
PascalManual + RC interfacesmanual New/Dispose; interfaces are RCCycles via interfaces
PrologTracingimpl-specific (SWI uses BDW-Boehm; SICStus precise)Term garbage in long predicates
TclRCinternal refcounted strings/lists/dictsMostly invisible
GroovyTracingJVM GCMetaClass retention
BashN/Ashell-managedSubshell leaks rare
PowerShellTracing.NET GCPipeline-buffer growth
SQLN/Aengine-managed (work_mem etc.)Query planner memory budgets
VMixedautofree (WIP) + opt-in GC + manualPre-1.0; default is GC
OdinManual + contextimplicit allocator in context; arena/temp/GPAForgetting to swap allocator
RocRC + uniquenessrefcount; uniqueness inference for in-place mutationPlatform-controlled
GleamTracing per-processinherits BEAM (Erlang)Same as Erlang
PonyRefcaps + per-actor GC6 capabilities; ORCA distributed GCCapability constraints
IdrisRC + linearrefcount; linear types for 1-use valuesQTT erasure
LeanRC FBIPrefcounted; in-place mutation when refcount=1FBIP only fires when proved unique
Coq (Rocq)TracingOCaml runtime (mostly)Proof terms can balloon
AgdaTracingGHC backend (mostly)Compile-time evaluation cost
SmalltalkImage + tracinggenerational GC inside image; image is the persistent heapImage bloat

Decision rubric

Pick manual when you must control every allocation: kernels, embedded, real-time audio/games, custom databases. C, Zig, C++ raw, Odin.

Pick RAII / ownership when you want compile-time safety and zero runtime cost: high-performance services, browsers, OS components. Rust, modern C++.

Pick refcount when you want predictable destruction without compile-time complexity, AND your data is mostly tree-shaped: iOS apps (Swift), small-data scripts. Cycles are the trap.

Pick tracing GC when allocator complexity isn’t worth your engineering time: business logic, web services, data pipelines, most application code. Java, C#, Go, Python, etc.

Pick pauseless GC when you have a big heap AND tight latency requirements: HFT, real-time analytics, low-latency networking. Java ZGC, Go.

Pick per-process heap (BEAM) when you have many independent activities (millions of connections, etc.) and want fault isolation: Erlang, Elixir.

Pick image-based when you want a live system with no source-runtime split, typically for research, exploratory programming, simulation: Smalltalk, Common Lisp.

Pick refcaps / linear types when data-race freedom must be proven, not tested: Pony for actors, Idris/Linear Haskell for resource-typed APIs.

Edge cases & nuances

  • A single language often spans categories. Java has G1 (gen-tracing), ZGC (pauseless), Parallel, Serial. Nim lets you pick arc, orc, boehm, etc. at compile time. Python’s free-threaded build (3.14) makes refcounts atomic and changes the contention story.
  • Compaction matters. Go and ZGC don’t compact (or compact differently); Ruby 3.x does. Compaction means addresses can move — a problem for FFI.
  • “Stop-the-world” is sometimes a misnomer. ZGC and Shenandoah have sub-ms STW phases but are >99% concurrent. Erlang’s whole-VM never STWs but each process pauses individually.
  • Weak references / finalizers are the last resort and have surprising semantics. Java’s finalize() is deprecated; use Cleaner. Python’s __del__ runs at refcount-0 (not GC), so cycles never fire it. Swift weak refs zero out automatically; Rust Weak requires upgrade.
  • The free-threaded Python (PEP 703) is a major shift: from refcount + GIL to atomic refcount + biased reference counting. As of 3.14 (2025-10) it’s stable; expect ecosystem fragmentation through 2027.
  • Lean 4’s FBIP is the most underappreciated memory innovation in mainstream programming languages — read the Counting Immutable Beans paper.
  • Pony and Roc are the two languages that found different paths to “no GC pause without ownership pain” — refcaps + per-actor GC (Pony), and uniqueness inference + RC (Roc).

Cross-references

For per-language detail, see each note’s Advanced section under “memory model & GC tuning”:

Citations