Ruby Memory — Line by Line Breakdown
Ruby Memory Internals — What Happens Per Line
Tracing exactly what Ruby allocates on stack and heap for each line of code.
Class Definition
class User
attr_accessor :name, :age, :hobbies
end
# HEAP: Class object "User" created (classes are objects in Ruby!)
# Method table allocated, getter/setter methods generated
# DATA: Constant "User" → points to class object on heap
# ObjectSpace: 1 new object (the User class itself)String Literal
greeting = "Hello, World!"
# STACK: greeting → [pointer to heap object]
# HEAP:
# RString object (40 bytes RVALUE slot)
# flags: T_STRING
# len: 13
# data: embedded (≤23 bytes fit inside RVALUE!)
#
# Short strings (≤23 bytes on 64-bit) are EMBEDDED
# directly in the slot — no extra malloc needed!Integer — No Heap Allocation!
age = 42
# STACK: age → 85 (NOT a pointer! Tagged value)
#
# Ruby "tagged pointers" for Fixnum:
# Stored as: (42 << 1) | 1 = 85
# Lowest bit = 1 means "immediate integer"
#
# HEAP: NOTHING allocated!
# Also immediate (no heap): nil, true, false, Symbol, FixnumSymbol — Global, Never Duplicated
status = :active
# STACK: status → [immediate value / symbol ID]
# GLOBAL SYMBOL TABLE: :active → ID 12345
# HEAP: NOTHING (if :active was seen before)
#
# Key difference:
# "active" → NEW String object EVERY time (heap alloc)
# :active → same symbol ID every time (no alloc)
#
# ⚠️ Symbols never GC'd (before Ruby 2.2)
# ✓ Dynamic symbols ARE collected since Ruby 2.2Array Creation — Multiple Allocations
hobbies = ["reading", "coding", "coffee"]
# STACK: hobbies → [pointer]
# HEAP:
# RArray (40 bytes RVALUE)
# len: 3
# elements: [ptr0, ptr1, ptr2]
# ↓ ↓ ↓
# RString RString RString
# "reading" "coding" "coffee"
#
# Total: 4 objects (1 Array + 3 Strings)
# Arrays ≤3 elements: EMBEDDED in RVALUE (no extra alloc)Hash — Symbols Are Free
config = { host: "localhost", port: 3000, debug: true }
# HEAP:
# RHash (40 bytes)
# :host → ptr to "localhost" (1 String alloc)
# :port → 6001 (immediate! 3000<<1|1, no alloc)
# :debug → true (immediate! no alloc)
#
# Total: 2 objects (1 Hash + 1 String)
# Symbols, integers, booleans = immediate = FREEObject Instantiation — Reference Sharing
user = User.new
user.name = "Alice"
user.age = 30
user.hobbies = hobbies
# HEAP:
# RObject (User instance)
# @name → ptr → "Alice" (new String)
# @age → 61 (immediate! 30<<1|1)
# @hobbies → ptr → SAME Array from earlier!
#
# ⚠️ user.hobbies = hobbies copies the POINTER, not data!
# user.hobbies.push("tea") mutates the SAME array
# hobbies == ["reading", "coding", "coffee", "tea"]Method Call — Stack Frames
def format_name(user)
prefix = "Mr."
"#{prefix} #{user.name}"
end
result = format_name(user)
# STACK during call:
# ┌─────────────────────────┐ ← main frame
# │ user → [ptr to User] │
# │ result → (pending) │
# ├─────────────────────────┤ ← format_name frame
# │ user → [same ptr!] │ (arg = pointer copy)
# │ prefix → [ptr to "Mr."]│
# │ return_addr │
# └─────────────────────────┘
#
# HEAP: "Mr." (new String), "Mr. Alice" (interpolation = new!)
# After return: frame popped, "Mr." eligible for GC
# "Mr. Alice" survives (result references it)Closure — Captures Environment
multiplier = 3
double_it = lambda { |x| x * multiplier }
double_it.call(5) # => 15
# HEAP:
# Proc (Lambda) object
# block: reference to compiled code
# binding: captured environment
# multiplier → reference (not copy!)
#
# ⚠️ Captures BINDING, not value:
# multiplier = 10
# double_it.call(5) # => 50 (uses current value!)
#
# Stack frame can't be freed if closure references its vars
# Ruby promotes captured vars to heap (binding keeps alive)String Operations — Watch Allocations!
name = "Alice" # 1 allocation
name2 = name # 0 allocs (same pointer!)
name3 = name.dup # 1 alloc (copy)
name4 = name + " Bob" # 1 alloc (new string)
name5 = "Alice" # 1 alloc (NEW object!)
name.equal?(name2) # true (same object)
name.equal?(name5) # false (different object!)
name == name5 # true (same content)
# frozen_string_literal: true (magic comment)
name7 = "Alice" # 0 allocs! (reuses frozen)
# ⚠️ This saves THOUSANDS of allocations in Rails apps!Garbage Collection
def process
temp = Array.new(1000) { |i| "item_#{i}" } # 1001 objects!
temp.first(3)
end
result = process
# After return:
# temp gone (stack popped)
# Array of 1000 → unreachable → eligible for GC
# 997 strings → unreachable → eligible for GC
# 3 strings survive (in result)
#
# Ruby GC: Mark-and-Sweep + Generational
# MARK: follow refs from roots (stack, globals)
# SWEEP: not marked → free it
# Generational: new objects collected more often
# (most die young = "infant mortality")Rails Request — Full Picture
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
@posts = @user.posts.limit(10)
render json: @user.as_json
end
end
# Per request allocations:
# params, request object ~20 objects
# SQL string, User instance ~50 objects
# ActiveRecord::Relation ~5 objects
# .as_json Hash, JSON String ~30 objects
# ─────────────────────────────────────────
# TOTAL: ~100-500 objects PER REQUEST
#
# At 1000 req/s = 100K-500K allocs/sec → GC pressure!
# This is why Rails is slower:
# - Many small allocations
# - GC pauses all threads (stop-the-world)
# - Ruby 3.x: incremental GC, less pauseRVALUE — Ruby's Object Slot
Every Ruby object = 40-byte slot (RVALUE):
┌──────────────────────────────────────────┐
│ flags (8 bytes) - type, frozen, etc. │
│ klass (8 bytes) - pointer to class │
│ payload (24 bytes) - type-specific: │
│ String: embedded chars (if ≤23 bytes) │
│ Array: embedded elements (if ≤3) │
│ Object: instance variable pointers │
└──────────────────────────────────────────┘
If data > 24 bytes → overflow to separate mallocRuby Memory Optimization
PatternAllocationsBetter Alternative"string" in loop1 per iterationfreeze or frozen_string_literal: truearray.map { }New array + N objectsarray.each { } if don't need resulthash.merge(other)New hashhash.merge!(other) (mutate)a + b + c (strings)2 intermediate strings"#{a}#{b}#{c}" or [a,b,c].joinUser.all.map(&:name)N User objectsUser.pluck(:name)JSON.parse(str)Many objectsOj.load(str, symbol_keys: true)Memory Profiling Tools
# Object count
ObjectSpace.count_objects
# => {:TOTAL=>50000, :T_STRING=>12000, ...}
# Memory profiler gem
require 'memory_profiler'
report = MemoryProfiler.report { your_code_here }
report.pretty_print
# GC stats
GC.stat
# => {:count=>15, :heap_allocated_pages=>100, ...}