CrackedRuby CrackedRuby

Memory Allocation Strategies

Overview

Memory allocation strategies determine how programs request, receive, and release memory during execution. These strategies balance competing requirements: speed of allocation, efficient memory usage, fragmentation prevention, and deterministic behavior. The choice of allocation strategy affects application performance, memory footprint, and system scalability.

Operating systems and runtime environments implement various allocation strategies, each optimized for different usage patterns. Static allocation assigns memory at compile time, stack allocation manages automatic variables through function calls, and heap allocation provides dynamic memory during runtime. Modern systems combine multiple strategies, selecting the appropriate mechanism based on object lifetime, size, and access patterns.

Memory allocation involves several key operations: requesting memory blocks, tracking allocated regions, managing free space, and reclaiming unused memory. Each strategy implements these operations differently, trading performance for flexibility or determinism for dynamic behavior.

# Memory allocation in Ruby - all allocations go through object creation
string = "allocated on heap"  # String object allocated
array = [1, 2, 3]              # Array and elements allocated
hash = {key: "value"}          # Hash structure allocated

# Ruby hides allocation details but manages memory automatically
class User
  def initialize(name)
    @name = name  # Instance variable storage allocated
  end
end

user = User.new("Alice")  # Object allocated by Ruby VM

The allocation strategy impacts application behavior in multiple dimensions. Allocation speed determines how quickly programs can create objects. Memory fragmentation affects long-running process stability. Overhead from metadata and alignment affects memory efficiency. Thread safety determines concurrent allocation capability.

Key Principles

Memory allocation strategies operate on fundamental principles that govern their behavior and implementation. Understanding these principles clarifies how different strategies make trade-offs between competing requirements.

Memory Lifetime Management

Objects have three distinct lifetime patterns: automatic, dynamic, and static. Automatic lifetime objects exist within a defined scope and deallocate when that scope exits. Dynamic lifetime objects persist until explicitly freed or garbage collected. Static lifetime objects exist for the entire program duration. Allocation strategies specialize for these patterns, optimizing common cases.

Address Space Organization

Programs divide their address space into regions with different allocation characteristics. The text segment contains executable code. The data segment stores global and static variables. The stack grows and shrinks automatically with function calls. The heap provides dynamically allocated memory. This separation enables specialized allocation strategies for each region.

Allocation Metadata

Allocators maintain metadata to track memory usage. Block headers store size information, allocation status, and links to adjacent blocks. Free lists connect available memory blocks. Boundary tags enable coalescing adjacent free blocks. The metadata overhead trades memory space for allocation speed and flexibility.

# Ruby's allocation metadata (simplified conceptual model)
# Actual implementation in C, hidden from Ruby code

class ObjectSpace
  def self.allocation_info
    # Ruby tracks object allocation internally
    objects = []
    
    ObjectSpace.each_object do |obj|
      # Metadata includes: class, size, references
      objects << {
        class: obj.class,
        object_id: obj.object_id,
        size: ObjectSpace.memsize_of(obj)
      }
    end
    
    objects
  end
end

Fragmentation Types

External fragmentation occurs when free memory exists but not in contiguous blocks large enough for requests. Internal fragmentation happens when allocated blocks contain unused space due to rounding or minimum sizes. Allocation strategies combat fragmentation through coalescing, compaction, or avoiding it entirely through fixed-size allocations.

Allocation Speed vs Memory Efficiency

Fast allocation often requires maintaining complex data structures or accepting higher memory overhead. Simple allocators minimize metadata but perform slower searches. Bump allocation provides constant-time allocation by incrementing a pointer but cannot reuse freed memory. Free list allocators reuse memory but require traversing lists.

Thread Safety Models

Single-threaded allocators avoid synchronization overhead. Per-thread allocators give each thread independent memory pools. Global allocators use locks to serialize access. Lock-free allocators employ atomic operations and careful ordering. The choice affects both performance and implementation complexity.

Allocation Granularity

Block size affects both fragmentation and allocation speed. Small blocks reduce internal fragmentation but increase metadata overhead. Large blocks minimize allocation count but increase external fragmentation. Variable-sized allocators adapt to requests but require complex management. Fixed-size allocators simplify management but may waste memory.

Implementation Approaches

Memory allocation strategies fall into several categories, each optimized for different usage patterns and requirements. Real systems often combine multiple approaches, selecting the strategy based on allocation characteristics.

Static Allocation

Static allocation assigns memory at compile time, storing data in the program's data segment. The compiler determines memory layout, and addresses remain fixed throughout execution. This approach eliminates runtime allocation overhead and provides deterministic memory usage.

// Static allocation (conceptual)
static_variable = reserved_at_compile_time
global_array[1000] = fixed_size_known_ahead

allocation_time: 0 (done at compile time)
deallocation_time: 0 (reclaimed when program exits)
memory_overhead: 0 (no metadata needed)

Static allocation works well for global variables, constant data, and embedded systems with known memory requirements. The strategy cannot accommodate variable-sized data or dynamic object creation, limiting its applicability in general-purpose programming.

Stack Allocation

Stack allocation manages automatic variables through a last-in-first-out discipline. Function calls push activation records containing local variables, and returns pop them off. Stack allocation provides constant-time operations by adjusting a single stack pointer.

function_call():
  stack_pointer -= frame_size
  store local variables at [stack_pointer]
  execute function body
  
function_return():
  stack_pointer += frame_size
  locals automatically deallocated

Stack allocation suits temporary variables with scope-bound lifetimes. The fixed stack size limits recursive depth and prevents allocating large objects. Stack discipline prevents flexible object lifetimes, requiring heap allocation for objects that outlive their creating function.

Heap Allocation with Free Lists

Free list allocators maintain linked lists of available memory blocks. Allocation searches the list for suitable blocks using first-fit, best-fit, or other policies. Deallocation returns blocks to the free list, potentially coalescing with adjacent free blocks.

# Conceptual free list implementation
class FreeListAllocator
  def initialize(memory_size)
    @free_list = [Block.new(0, memory_size)]
  end
  
  def allocate(size)
    # First-fit: find first block large enough
    @free_list.each_with_index do |block, index|
      if block.size >= size
        # Split block if larger than needed
        if block.size > size + MIN_BLOCK_SIZE
          new_block = Block.new(block.address + size, block.size - size)
          @free_list.insert(index + 1, new_block)
          block.size = size
        end
        
        return @free_list.delete_at(index)
      end
    end
    
    nil  # Out of memory
  end
  
  def free(block)
    # Insert block back into free list
    @free_list << block
    @free_list.sort_by!(&:address)
    coalesce_adjacent_blocks
  end
  
  def coalesce_adjacent_blocks
    i = 0
    while i < @free_list.length - 1
      current = @free_list[i]
      next_block = @free_list[i + 1]
      
      if current.address + current.size == next_block.address
        current.size += next_block.size
        @free_list.delete_at(i + 1)
      else
        i += 1
      end
    end
  end
end

Free list allocation balances flexibility and performance. First-fit searches quickly but may fragment memory. Best-fit minimizes wasted space but requires full list traversal. Worst-fit leaves larger remaining blocks but increases fragmentation. The strategy adapts to variable-sized allocations but requires coalescing to combat fragmentation.

Buddy System Allocation

Buddy allocators partition memory into power-of-two sized blocks. Allocation rounds requests up to the next power of two and splits larger blocks recursively. Deallocation merges freed blocks with their buddies if both are free.

# Buddy allocator (simplified)
class BuddyAllocator
  def initialize(total_size)
    @order = Math.log2(total_size).to_i
    @free_lists = Array.new(@order + 1) { [] }
    @free_lists[@order] << Block.new(0, total_size)
  end
  
  def allocate(size)
    # Round up to next power of 2
    order = (Math.log2(size).ceil)
    order = 0 if order < 0
    
    # Find smallest available block
    current_order = order
    while current_order <= @order && @free_lists[current_order].empty?
      current_order += 1
    end
    
    return nil if current_order > @order
    
    # Split blocks until reaching desired size
    while current_order > order
      block = @free_lists[current_order].pop
      current_order -= 1
      
      buddy_size = 2 ** current_order
      @free_lists[current_order] << Block.new(block.address, buddy_size)
      @free_lists[current_order] << Block.new(block.address + buddy_size, buddy_size)
    end
    
    @free_lists[order].pop
  end
  
  def free(block)
    order = Math.log2(block.size).to_i
    
    # Try to merge with buddy
    while order < @order
      buddy_address = block.address ^ (2 ** order)
      buddy = @free_lists[order].find { |b| b.address == buddy_address }
      
      break unless buddy
      
      @free_lists[order].delete(buddy)
      block = Block.new([block.address, buddy_address].min, 2 ** (order + 1))
      order += 1
    end
    
    @free_lists[order] << block
  end
end

Buddy allocation simplifies coalescing by constraining block addresses. The power-of-two sizes cause internal fragmentation but enable fast buddy calculation. The strategy suits systems with varied allocation sizes and long-running processes requiring fragmentation resistance.

Slab Allocation

Slab allocators pre-allocate memory for fixed-size objects, eliminating allocation overhead for frequently created types. Each slab contains multiple objects of identical size, and allocators maintain lists of slabs with available objects.

# Slab allocator for fixed-size objects
class SlabAllocator
  def initialize(object_size, slab_size = 4096)
    @object_size = object_size
    @objects_per_slab = slab_size / object_size
    @slabs = []
    @free_objects = []
  end
  
  def allocate
    if @free_objects.empty?
      allocate_new_slab
    end
    
    @free_objects.pop
  end
  
  def free(object)
    @free_objects << object
  end
  
  private
  
  def allocate_new_slab
    slab = Slab.new(@object_size, @objects_per_slab)
    @slabs << slab
    @free_objects.concat(slab.objects)
  end
end

# Used internally by many systems for common sizes
small_object_allocator = SlabAllocator.new(32)
medium_object_allocator = SlabAllocator.new(128)

Slab allocation excels for kernel object caches and memory pools. The fixed-size constraint limits applicability but enables constant-time allocation without fragmentation. Modern allocators combine slabs for common sizes with general allocators for unusual requests.

Region-Based Allocation

Region allocators (arena allocators) allocate memory in bulk and free entire regions at once. Individual objects within a region cannot be freed separately, simplifying allocation and eliminating per-object metadata.

class RegionAllocator
  def initialize
    @regions = []
    @current_region = nil
    @region_offset = 0
  end
  
  def allocate(size)
    if @current_region.nil? || @region_offset + size > REGION_SIZE
      @current_region = allocate_region(REGION_SIZE)
      @regions << @current_region
      @region_offset = 0
    end
    
    object_address = @current_region + @region_offset
    @region_offset += size
    object_address
  end
  
  def free_all
    @regions.each { |region| free_region(region) }
    @regions.clear
    @current_region = nil
    @region_offset = 0
  end
end

Region allocation suits request-response systems, compilers, and batch processing where many objects share a common lifetime. The inability to free individual objects wastes memory for long-lived regions with mixed lifetimes.

Ruby Implementation

Ruby implements automatic memory management through a garbage collector, abstracting allocation details from developers. The Ruby VM handles object allocation, tracks references, and reclaims unreachable objects. Understanding Ruby's allocation strategy helps optimize memory usage and diagnose performance issues.

Object Allocation Model

Ruby allocates all objects on the heap, managed by the VM. Object creation triggers allocation through the VM's internal allocator, which maintains memory pools organized by object size. Small objects use slab-like allocators, while large objects use direct system allocations.

# All Ruby objects allocated on heap
string = "text"           # String object allocated
array = Array.new(100)    # Array structure + element storage
hash = Hash.new           # Hash table allocated

# Even seemingly "simple" values create objects
number = 42              # Fixnum: small integers (optimized, not always allocated)
float = 3.14            # Float: always allocated
symbol = :name          # Symbol: allocated once, reused

# Object allocation visible through ObjectSpace
require 'objspace'

obj = "allocated string"
puts ObjectSpace.memsize_of(obj)  # Size in bytes
# => 40

# Allocation tracking
ObjectSpace.trace_object_allocations_start

1000.times { "string #{rand}" }

allocations = []
ObjectSpace.each_object(String) do |str|
  if ObjectSpace.allocation_sourcefile(str)
    allocations << {
      file: ObjectSpace.allocation_sourcefile(str),
      line: ObjectSpace.allocation_sourceline(str),
      class: ObjectSpace.allocation_class_path(str)
    }
  end
end

ObjectSpace.trace_object_allocations_stop

Ruby optimizes small integer allocation through immediate values, storing Fixnums directly in pointers rather than allocating objects. Symbols receive similar treatment after initial allocation, reusing the same object for repeated symbol references. These optimizations reduce allocation overhead for common cases.

Generational Garbage Collection

Ruby uses generational garbage collection, dividing objects into generations based on age. New objects start in the young generation, and survivors promote to old generations after surviving multiple collections. This strategy optimizes for the observation that most objects die young.

# GC.stat shows allocation and collection statistics
require 'objspace'

before = GC.stat

# Allocate many temporary objects
10_000.times do
  temp = "temporary string #{rand}"
  temp.upcase
end

after = GC.stat

puts "Minor GC runs: #{after[:minor_gc_count] - before[:minor_gc_count]}"
puts "Major GC runs: #{after[:major_gc_count] - before[:major_gc_count]}"
puts "Objects allocated: #{after[:total_allocated_objects] - before[:total_allocated_objects]}"
puts "Heap pages: #{after[:heap_allocated_pages]}"

# Control GC behavior
GC.start                    # Force immediate collection
GC.disable                  # Disable automatic GC
# ... allocate objects ...
GC.enable                   # Re-enable GC
GC.compact                  # Compact heap (Ruby 2.7+)

Minor collections scan only the young generation, running frequently with low pause times. Major collections scan all generations, running less frequently with longer pauses. The generational hypothesis—most objects die young—makes this strategy effective for typical Ruby programs.

Heap Slot Management

Ruby's heap consists of pages containing fixed-size slots. Objects smaller than the slot size fit within a single slot, while larger objects span multiple slots or use separate large object allocation. The VM maintains free lists of available slots, organized by size class.

# Heap slot information through GC module
heap_info = GC.stat_heap

heap_info.each do |slot_size, stats|
  puts "Slot size: #{slot_size}"
  puts "  Total slots: #{stats[:heap_allocatable_slots]}"
  puts "  Used slots: #{stats[:heap_eden_slots]}"
  puts "  Free slots: #{stats[:heap_free_slots]}"
end

# Observe slot allocation over time
class SlotMonitor
  def self.monitor
    loop do
      stats = GC.stat
      
      puts "Heap pages: #{stats[:heap_allocated_pages]}"
      puts "Live objects: #{stats[:heap_live_slots]}"
      puts "Free objects: #{stats[:heap_free_slots]}"
      puts "Heap fragmentation: #{calculate_fragmentation(stats)}"
      
      sleep 1
    end
  end
  
  def self.calculate_fragmentation(stats)
    total_slots = stats[:heap_live_slots] + stats[:heap_free_slots]
    return 0 if total_slots == 0
    
    (stats[:heap_free_slots].to_f / total_slots * 100).round(2)
  end
end

Object size classes determine slot assignment. Small objects (strings, arrays with few elements) use standard slots. Medium objects may require multiple slots. Large objects (big arrays, long strings) bypass the slot system, using direct system allocations. This multi-tier approach balances allocation speed and memory efficiency.

Memory Pool Management

Ruby maintains separate memory pools for different object types, reducing contention and improving cache locality. Each pool uses appropriate allocation strategies for its object characteristics.

# Monitor memory pools through allocation tracing
require 'objspace'

ObjectSpace.trace_object_allocations_start

# Generate various object types
strings = 1000.times.map { |i| "string #{i}" }
arrays = 1000.times.map { |i| [i, i*2, i*3] }
hashes = 1000.times.map { |i| {key: i, value: i*2} }

# Analyze allocation patterns
allocation_stats = Hash.new(0)

ObjectSpace.each_object do |obj|
  next unless ObjectSpace.allocation_sourcefile(obj)
  
  class_name = obj.class.name
  size = ObjectSpace.memsize_of(obj)
  allocation_stats["#{class_name}:#{size}"] += 1
end

ObjectSpace.trace_object_allocations_stop

allocation_stats.sort_by { |k, v| -v }.first(10).each do |type, count|
  puts "#{type}: #{count} allocations"
end

Memory pools reduce fragmentation by grouping similar objects. Strings allocate from string pools, arrays from array pools, and so on. This organization improves allocation speed by avoiding searches across differently-typed objects.

Allocation Optimization Strategies

Ruby developers can optimize allocation patterns by reusing objects, preallocating structures, and avoiding unnecessary copies. These strategies reduce garbage collection pressure and improve performance.

# Inefficient: creates many temporary strings
def build_message_slow(items)
  message = ""
  items.each do |item|
    message += item.to_s  # Creates new string each iteration
  end
  message
end

# Efficient: mutates single string
def build_message_fast(items)
  message = ""
  items.each do |item|
    message << item.to_s  # Mutates existing string
  end
  message
end

# Even better: preallocate capacity
def build_message_optimized(items)
  message = String.new(capacity: items.sum { |i| i.to_s.size })
  items.each do |item|
    message << item.to_s
  end
  message
end

# Array preallocation
def process_data_slow(count)
  results = []
  count.times { |i| results << compute(i) }
  results
end

def process_data_fast(count)
  results = Array.new(count)
  count.times { |i| results[i] = compute(i) }
  results
end

# Object pooling for frequently created objects
class ConnectionPool
  def initialize(size)
    @pool = Array.new(size) { create_connection }
    @available = @pool.dup
  end
  
  def acquire
    @available.pop || create_connection
  end
  
  def release(connection)
    @available << connection if @available.size < @pool.size
  end
end

These optimizations reduce allocation rate, decreasing GC frequency and improving application throughput. Profiling tools identify allocation hotspots, guiding optimization efforts.

Performance Considerations

Memory allocation performance affects application speed, scalability, and predictability. Allocation strategy choices create performance trade-offs across multiple dimensions: allocation speed, deallocation speed, memory overhead, fragmentation, and thread scalability.

Allocation Latency

Allocation time varies significantly across strategies. Bump allocators provide constant-time allocation by incrementing a pointer. Free list allocators require searching for suitable blocks, with time proportional to list length. Segregated fits reduce search time by maintaining separate lists for size classes.

# Benchmark allocation strategies
require 'benchmark'

ITERATIONS = 100_000

# Measure Ruby's built-in allocation
Benchmark.bmbm do |x|
  x.report("String allocation") do
    ITERATIONS.times { "test string" }
  end
  
  x.report("Array allocation") do
    ITERATIONS.times { [1, 2, 3, 4, 5] }
  end
  
  x.report("Hash allocation") do
    ITERATIONS.times { {a: 1, b: 2, c: 3} }
  end
  
  x.report("Object allocation") do
    ITERATIONS.times { Object.new }
  end
end

# Compare reuse vs allocation
Benchmark.bmbm do |x|
  x.report("New strings") do
    ITERATIONS.times { |i| "string #{i}" }
  end
  
  x.report("Reused string") do
    str = ""
    ITERATIONS.times { |i| str.replace("string #{i}") }
  end
  
  x.report("String mutation") do
    str = ""
    ITERATIONS.times { |i| str.clear << "string" << i.to_s }
  end
end

Allocation speed impacts high-frequency operations. Web servers handling thousands of requests per second spend significant time allocating request objects. Game engines allocating entities each frame require fast allocation. Database systems allocating query results need efficient memory management.

Deallocation Overhead

Explicit deallocation requires traversing data structures, coalescing adjacent blocks, and updating free lists. Garbage collection defers deallocation costs, paying them in batches during collection cycles. The trade-off between immediate overhead and batched pauses depends on application requirements.

# Measure GC impact on performance
def measure_gc_impact
  GC.disable
  
  no_gc_time = Benchmark.measure do
    10_000.times do
      array = Array.new(1000) { |i| "element #{i}" }
      process_array(array)
    end
  end
  
  GC.enable
  GC.start  # Start fresh
  
  with_gc_time = Benchmark.measure do
    10_000.times do
      array = Array.new(1000) { |i| "element #{i}" }
      process_array(array)
    end
  end
  
  puts "Without GC: #{no_gc_time.real}s"
  puts "With GC: #{with_gc_time.real}s"
  puts "GC overhead: #{((with_gc_time.real / no_gc_time.real - 1) * 100).round(2)}%"
end

Deallocation speed matters for short-lived applications and batch jobs. Region-based allocation eliminates individual deallocation by freeing entire regions. Generational GC reduces pause times by collecting only young objects frequently. Manual memory management provides predictable deallocation timing but requires careful programming.

Memory Fragmentation Impact

Fragmentation reduces effective memory capacity and increases allocation failures. External fragmentation leaves memory unusable despite sufficient total free space. Internal fragmentation wastes allocated space through rounding and minimum block sizes.

# Demonstrate fragmentation effects
class FragmentationDemo
  def self.demonstrate
    # Allocate mixed sizes
    large_objects = 100.times.map { "x" * 10_000 }
    small_objects = 1000.times.map { "x" * 100 }
    
    before_stats = GC.stat
    
    # Free alternating objects (creates fragmentation)
    large_objects.each_with_index { |obj, i| obj.clear if i.even? }
    small_objects.each_with_index { |obj, i| obj.clear if i.odd? }
    
    GC.start
    
    after_stats = GC.stat
    
    puts "Heap pages before: #{before_stats[:heap_allocated_pages]}"
    puts "Heap pages after: #{after_stats[:heap_allocated_pages]}"
    puts "Free slots: #{after_stats[:heap_free_slots]}"
    puts "Live slots: #{after_stats[:heap_live_slots]}"
    
    # Fragmentation prevents page reclamation
    if after_stats[:heap_allocated_pages] == before_stats[:heap_allocated_pages]
      puts "Fragmentation prevented page release"
    end
  end
end

Long-running processes accumulate fragmentation over time. Buddy systems limit fragmentation through constrained block sizes. Compacting collectors move objects to eliminate external fragmentation. Slab allocation avoids fragmentation entirely for fixed-size objects.

Cache Effects

Memory allocation patterns affect CPU cache performance. Sequential allocations improve cache hit rates through spatial locality. Scattered allocations cause cache misses. Object pooling and arena allocation improve locality by grouping related objects.

# Compare cache-friendly vs cache-unfriendly allocation
class CacheEffectsDemo
  def self.sequential_processing
    # Allocate in sequence
    data = Array.new(10_000) { |i| {id: i, value: rand} }
    
    # Process sequentially (cache-friendly)
    sum = 0
    data.each { |item| sum += item[:value] }
    sum
  end
  
  def self.random_processing
    # Allocate in sequence
    data = Array.new(10_000) { |i| {id: i, value: rand} }
    indices = (0...10_000).to_a.shuffle
    
    # Process randomly (cache-unfriendly)
    sum = 0
    indices.each { |i| sum += data[i][:value] }
    sum
  end
end

# Sequential access benefits from cache lines loading adjacent data
# Random access causes more cache misses

Allocation strategy affects cache behavior indirectly through object placement. Generational collectors group young objects, improving locality for frequently accessed data. Segregated fits separate different size classes, potentially hurting locality. Thread-local allocation improves cache affinity by keeping thread data on the same CPU.

Scalability with Thread Count

Memory allocation scalability determines multi-threaded application performance. Global allocators serialize all allocation through locks, limiting scalability. Per-thread allocators eliminate contention but may waste memory. Lock-free allocators balance these concerns through atomic operations.

# Demonstrate allocation contention effects
require 'concurrent'

def measure_allocation_scaling
  [1, 2, 4, 8].each do |thread_count|
    time = Benchmark.measure do
      threads = thread_count.times.map do
        Thread.new do
          10_000.times do
            array = Array.new(100) { rand }
            array.sum
          end
        end
      end
      
      threads.each(&:join)
    end
    
    allocations_per_second = (thread_count * 10_000 / time.real).to_i
    puts "#{thread_count} threads: #{allocations_per_second} allocs/sec"
  end
end

Thread-local pools reduce contention by giving each thread independent memory. Object stealing allows load balancing when threads have imbalanced allocation patterns. The trade-off between contention and memory waste depends on allocation frequency and thread count.

Common Patterns

Memory allocation patterns emerge from common requirements and constraints. These patterns codify effective approaches to allocation problems, providing reusable solutions for typical scenarios.

Object Pooling

Object pools maintain collections of reusable objects, reducing allocation frequency for expensive or frequently created objects. Pools trade memory for allocation speed, keeping objects alive between uses.

class ObjectPool
  def initialize(size, &factory)
    @factory = factory
    @pool = Array.new(size) { @factory.call }
    @available = @pool.dup
    @mutex = Mutex.new
  end
  
  def acquire
    @mutex.synchronize do
      @available.pop || @factory.call
    end
  end
  
  def release(obj)
    @mutex.synchronize do
      @available << obj if @available.size < @pool.size
    end
  end
  
  def with_object
    obj = acquire
    begin
      yield obj
    ensure
      release(obj)
    end
  end
end

# Database connection pool
connection_pool = ObjectPool.new(10) { Database.new_connection }

connection_pool.with_object do |conn|
  conn.execute("SELECT * FROM users")
end

# Thread pool with preallocated threads
thread_pool = ObjectPool.new(4) { Thread.new { |work| work.call } }

Object pools suit resource handles (database connections, file handles), complex objects with expensive initialization, and high-frequency temporary objects. The pool size balances memory usage against allocation savings.

Arena Allocation for Request Handling

Arena allocation simplifies memory management for request-response systems by allocating all request data from a single region and freeing it after response completion.

class RequestArena
  def initialize
    @objects = []
  end
  
  def allocate_string(content)
    str = String.new(content)
    @objects << str
    str
  end
  
  def allocate_array(size)
    arr = Array.new(size)
    @objects << arr
    arr
  end
  
  def clear
    @objects.clear
  end
end

class RequestHandler
  def handle_request(request)
    arena = RequestArena.new
    
    # All allocations use arena
    parsed_data = arena.allocate_array(request.params.size)
    request.params.each do |key, value|
      processed = arena.allocate_string(process_value(value))
      parsed_data << processed
    end
    
    response = generate_response(parsed_data)
    
    # Free all request allocations at once
    arena.clear
    
    response
  end
end

Arena allocation suits web servers, RPC handlers, and batch processors where many objects share a common lifetime. The pattern eliminates individual deallocation calls and improves locality by grouping related allocations.

Copy-on-Write Optimization

Copy-on-write defers copying until modification, sharing memory between readers and creating copies only when necessary. This pattern reduces allocation for immutable or rarely modified data.

class CopyOnWriteArray
  def initialize(array = [])
    @data = array
    @copied = false
  end
  
  def [](index)
    @data[index]
  end
  
  def []=(index, value)
    ensure_writable_copy
    @data[index] = value
  end
  
  def <<(value)
    ensure_writable_copy
    @data << value
  end
  
  def dup
    # Share underlying array until write
    copy = CopyOnWriteArray.new(@data)
    copy
  end
  
  private
  
  def ensure_writable_copy
    unless @copied
      @data = @data.dup
      @copied = true
    end
  end
end

# Ruby strings use copy-on-write in some implementations
original = "shared string"
copy = original.dup  # Shares memory initially

copy << " modified"  # Triggers actual copy

Copy-on-write applies to process forking, immutable data structures, and snapshot isolation. Operating systems use copy-on-write for process memory after fork, copying pages only when written. The pattern trades page fault overhead for reduced memory usage.

Size-Class Segregation

Segregated allocation maintains separate pools for different size classes, reducing fragmentation and improving allocation speed through specialized allocators for common sizes.

class SegregatedAllocator
  SIZE_CLASSES = [16, 32, 64, 128, 256, 512, 1024, 2048]
  
  def initialize
    @allocators = {}
    SIZE_CLASSES.each do |size|
      @allocators[size] = SlabAllocator.new(size)
    end
    @large_allocator = LargeObjectAllocator.new
  end
  
  def allocate(size)
    size_class = SIZE_CLASSES.find { |s| s >= size }
    
    if size_class
      @allocators[size_class].allocate
    else
      @large_allocator.allocate(size)
    end
  end
  
  def free(ptr, size)
    size_class = SIZE_CLASSES.find { |s| s >= size }
    
    if size_class
      @allocators[size_class].free(ptr)
    else
      @large_allocator.free(ptr)
    end
  end
end

Segregated allocation enables fast allocation by eliminating size searches. Small object allocators use simple free lists or bitmaps. Large object allocators handle unusual sizes with general-purpose strategies. The pattern underlies many modern allocators including jemalloc and tcmalloc.

Lazy Allocation

Lazy allocation defers memory allocation until actual use, reducing memory footprint for sparse or rarely-used data structures. Virtual memory systems implement lazy allocation through demand paging.

class LazyArray
  def initialize(size, &factory)
    @size = size
    @factory = factory
    @storage = {}
  end
  
  def [](index)
    return nil if index < 0 || index >= @size
    
    @storage[index] ||= @factory.call(index)
  end
  
  def []=(index, value)
    return if index < 0 || index >= @size
    
    @storage[index] = value
  end
end

# Allocate only accessed elements
sparse_data = LazyArray.new(1_000_000) { |i| expensive_computation(i) }
sparse_data[100]  # Only allocates element 100

# Lazy hash with default factory
lazy_cache = Hash.new { |h, k| h[k] = fetch_from_database(k) }
lazy_cache[:user_123]  # Allocates only when accessed

Lazy allocation applies to sparse matrices, caches, and configuration systems where not all data gets accessed. The pattern trades lookup overhead for reduced memory usage.

Reference

Allocation Strategy Comparison

Strategy Allocation Time Deallocation Time Fragmentation Memory Overhead Use Case
Static Compile-time Program exit None None Global data, constants
Stack O(1) O(1) None Minimal Local variables, call frames
Bump/Linear O(1) All-at-once None Minimal Arena allocation, temporary data
Free List O(n) O(1) High Medium General-purpose heap
Buddy System O(log n) O(log n) Medium Low Kernel memory, page allocation
Slab O(1) O(1) None Medium Fixed-size objects, kernel caches
Segregated Fits O(1) O(1) Low Medium Modern general allocators

Ruby Memory Management Methods

Method Purpose Example
GC.start Force garbage collection GC.start
GC.disable Disable automatic GC GC.disable
GC.enable Enable automatic GC GC.enable
GC.stat Get GC statistics GC.stat[:heap_live_slots]
GC.compact Compact heap (Ruby 2.7+) GC.compact
ObjectSpace.memsize_of Get object size in bytes ObjectSpace.memsize_of(obj)
ObjectSpace.each_object Iterate all objects ObjectSpace.each_object(String)
ObjectSpace.trace_object_allocations_start Track allocations ObjectSpace.trace_object_allocations_start

Fragmentation Types

Type Description Cause Mitigation
External Free memory exists but not in large enough contiguous blocks Scattered deallocations Coalescing, compaction
Internal Allocated blocks contain unused space Rounding, minimum block sizes Better size classes, exact-fit allocation
Heap Heap grows but cannot shrink Mixed object lifetimes Generational GC, compaction

Performance Characteristics

Factor Impact Fast Approach Slow Approach
Allocation Speed Throughput Bump pointer Free list search
Deallocation Speed Pause time Deferred (GC) Immediate with coalescing
Fragmentation Long-term stability Fixed sizes, compaction Variable sizes, no coalescing
Memory Overhead Memory efficiency Minimal metadata Per-object headers
Thread Scalability Multi-core performance Per-thread pools Global lock
Locality Cache performance Sequential allocation Scattered allocation

Ruby GC Statistics

Stat Key Meaning Interpretation
heap_allocated_pages Total heap pages allocated Memory committed to Ruby
heap_live_slots Slots containing live objects Current object count
heap_free_slots Empty slots available Allocation capacity
minor_gc_count Young generation collections Frequency of minor GC
major_gc_count Full heap collections Frequency of major GC
total_allocated_objects Cumulative object creations Allocation pressure
malloc_increase_bytes External memory growth C extension memory

Allocation Pattern Selection

Requirement Recommended Strategy Rationale
Deterministic timing Static or stack Zero runtime overhead
High allocation rate Bump pointer with periodic reset Constant-time allocation
Mixed object lifetimes Generational GC Optimizes common case
Fixed-size objects Slab allocation Eliminates fragmentation
Large temporary data Arena allocation Batch deallocation
Long-lived process Compacting GC Prevents fragmentation
Multi-threaded Per-thread pools Reduces contention
Memory-constrained Best-fit or segregated fits Minimizes waste

Common Allocation Sizes (Ruby)

Object Type Typical Size Allocation Strategy
Empty object 40 bytes Standard slot
Short string 40-80 bytes String pool
Small array 40 + (8 × elements) bytes Array pool
Hash 200+ bytes (varies with size) Hash pool
Symbol Allocated once Symbol table
Fixnum No allocation (immediate value) Immediate value
Float 24 bytes Float pool