Hello World

Be Happy!

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

Symbol — 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.2

Array 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 = FREE

Object 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 pause

RVALUE — 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 malloc

Ruby 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, ...}
#ruby (18) #rails (43) #memory (3) #heap (3) #stack (4) #gc (1) #rvalue (1) #optimization (1)
List