CrackedRuby CrackedRuby

Overview

Virtual memory is a memory management technique implemented by operating systems in collaboration with hardware memory management units (MMUs). The system creates an abstraction layer between the physical RAM available on a machine and the memory addresses that programs use during execution. Each process receives its own isolated virtual address space, typically much larger than the actual physical memory available.

The operating system and MMU work together to translate virtual addresses used by programs into physical addresses in RAM. This translation happens transparently, allowing programs to operate as if they have access to large, contiguous blocks of memory regardless of how fragmented or limited the physical memory actually is.

Virtual memory serves multiple critical functions in modern computing. It provides memory isolation between processes, preventing one application from accessing or corrupting another's memory space. It enables the operating system to use disk storage as an extension of RAM through paging or swapping mechanisms. It also allows for memory sharing between processes when appropriate, such as sharing read-only code segments of common libraries.

# Ruby programs run in virtual memory managed by the OS
# This allocation happens in the process's virtual address space
large_array = Array.new(10_000_000, 0)

# The OS may not allocate physical RAM immediately (lazy allocation)
# Physical pages are allocated on first access
large_array[5_000_000] = 42  # Triggers physical page allocation

The concept emerged in the 1960s as computers began running multiple programs concurrently and physical memory became a constraining resource. Virtual memory allows systems to run programs whose combined memory requirements exceed physical RAM capacity.

Key Principles

Virtual memory operates on several fundamental principles that define its behavior and capabilities. The virtual address space represents the range of memory addresses available to a process. On 64-bit systems, this space can be enormous (typically 2^48 or 2^64 addresses theoretically), far exceeding physical RAM. The operating system divides this space into user space and kernel space, with strict protections preventing user programs from accessing kernel memory.

Physical memory is divided into fixed-size units called page frames, typically 4KB in size though larger page sizes (2MB, 1GB) exist for specific use cases. The virtual address space is similarly divided into pages that map to these physical frames. The mapping between virtual pages and physical frames is stored in page tables maintained by the operating system for each process.

The Memory Management Unit (MMU) is a hardware component that performs address translation. When a program accesses a memory address, the MMU translates the virtual address to a physical address using the current process's page table. This translation occurs for every memory access, making it performance-critical. To accelerate translation, the MMU contains a Translation Lookaside Buffer (TLB), a small cache of recent virtual-to-physical address translations.

# Ruby's memory allocations go through virtual memory
class DataProcessor
  def initialize
    # Each object allocation gets virtual memory addresses
    @buffer = String.new(capacity: 1024 * 1024)  # 1MB string buffer
    @cache = {}
  end
  
  def process(data)
    # Memory access patterns affect page faults and TLB hits
    @buffer.clear
    @buffer << data.transform
    @cache[data.key] = @buffer.dup
  end
end

Page faults occur when a program accesses a virtual address whose corresponding page is not currently in physical memory. The MMU triggers a page fault exception, transferring control to the operating system's page fault handler. Minor page faults occur when the page exists in memory but isn't mapped in the page table. Major page faults require loading the page from disk.

Memory protection is enforced through permission bits in page table entries. Each page can be marked as readable, writable, executable, or combinations thereof. Attempts to violate these permissions trigger protection faults. This mechanism prevents buffer overflow exploits from executing injected code and isolates processes from each other.

The working set of a process consists of the pages currently in active use. Operating systems track working sets to make intelligent decisions about which pages to keep in physical memory. Pages outside the working set become candidates for eviction when memory pressure increases.

Demand paging defers loading pages into physical memory until they are accessed. When a process starts, the operating system doesn't immediately load all its code and data. Instead, pages are loaded on-demand as the program accesses them. This approach reduces startup time and memory usage for large applications.

Copy-on-write (COW) optimization allows multiple processes to share the same physical pages as long as they only read from them. When a process attempts to write to a shared page, the operating system creates a private copy for that process. This technique is used extensively in process creation (fork) and memory-mapped files.

Implementation Approaches

Operating systems implement virtual memory through several distinct strategies, each with different characteristics and trade-offs. The choice of implementation affects performance, memory utilization, and system complexity.

Paging divides both virtual and physical memory into fixed-size pages. This approach dominates modern operating systems due to its simplicity and flexibility. The fixed page size eliminates external fragmentation and simplifies memory allocation. However, internal fragmentation can occur when allocated memory doesn't perfectly fill pages. Multi-level page tables reduce memory overhead for sparse address spaces. A 64-bit address might be divided into multiple page table indices, allowing the system to allocate page table memory only for regions actually in use.

# Ruby's ObjectSpace provides insight into memory usage patterns
require 'objspace'

# Allocate objects and observe memory behavior
objects = 100_000.times.map { |i| "Object #{i}" * 10 }

# Check memory statistics
stats = ObjectSpace.memsize_of_all
puts "Total memory: #{stats} bytes"

# GC stats show interaction with virtual memory
GC.stat.each { |k, v| puts "#{k}: #{v}" if k.to_s.include?('heap') }

Segmentation divides memory into variable-sized segments based on logical program structures (code, stack, heap, data). Each segment has its own base address and length. While segmentation provides better logical organization and can reduce memory waste, it suffers from external fragmentation as segments are allocated and freed. Modern systems rarely use pure segmentation, though x86 architectures retain segmentation for backward compatibility.

Hybrid approaches combine paging and segmentation. Intel's x86-64 architecture supports both mechanisms, though 64-bit operating systems typically use paging exclusively with minimal segmentation. Some systems use segmentation for coarse-grained protection and paging for memory management.

Inverted page tables store one entry per physical page frame rather than per virtual page. This approach reduces page table memory overhead on systems with large address spaces but complicates address translation and sharing. The system must search the inverted page table to find the physical frame for a given virtual address, typically using hash tables for acceleration.

Superpages or huge pages use larger page sizes (2MB, 1GB) to reduce TLB misses and page table overhead for memory-intensive applications. Database systems and virtual machine hypervisors commonly use huge pages. The trade-off is increased internal fragmentation and less granular memory management.

# Memory-mapped files use virtual memory mechanisms
require 'fiddle'

# Map a file into process memory
File.write('data.bin', 'x' * 1024 * 1024)  # 1MB file

# Use Fiddle to demonstrate memory mapping concepts
# In practice, Ruby's File class handles this transparently
file = File.open('data.bin', 'r+')
data = file.read

# Modifications to memory-mapped regions are reflected in the file
# Ruby abstracts this, but the OS uses virtual memory for file I/O
data[0, 10] = 'Modified!!'
File.write('data.bin', data)

Shadow page tables are used in virtualization. The hypervisor maintains shadow page tables that map guest virtual addresses directly to host physical addresses, eliminating a layer of indirection. Nested paging (AMD's RVI or Intel's EPT) provides hardware support for two-level address translation, improving virtualization performance.

Ruby Implementation

Ruby applications run within virtual memory managed by the operating system, but Ruby provides several mechanisms to interact with and leverage virtual memory features. Understanding these interactions helps optimize Ruby applications and diagnose memory-related issues.

Ruby's garbage collector operates within the virtual memory system. When Ruby allocates objects, it requests memory from the operating system through system calls like mmap or sbrk. The OS allocates virtual memory pages, though physical pages may not be immediately committed. Ruby's heap management then subdivides these large allocations into slots for individual objects.

# Ruby's GC configuration affects virtual memory usage
GC.stat(:heap_allocated_pages)  # Number of pages allocated from OS
GC.stat(:heap_available_slots)  # Object slots available
GC.stat(:heap_live_slots)       # Currently used slots

# Inspect object memory layout
require 'objspace'

obj = { data: 'x' * 1000, metadata: { type: 'example' } }
puts "Object size: #{ObjectSpace.memsize_of(obj)} bytes"

# Force GC to interact with virtual memory
GC.start

Memory-mapped I/O allows Ruby programs to access files as if they were in memory. The operating system maps file contents into the process's virtual address space. Reading from these addresses causes page faults that load file data on demand. This approach provides efficient file access for large files without loading the entire contents into RAM.

# Memory-mapped file access through Ruby's IO
File.open('large_file.dat', 'rb') do |file|
  # Ruby buffers I/O, but OS uses memory mapping underneath
  # Seeking and reading specific regions is efficient
  file.seek(1024 * 1024 * 100)  # Skip to 100MB
  chunk = file.read(4096)        # Read 4KB page
end

# For more control, use external gems or FFI
require 'fiddle/import'

module MMap
  extend Fiddle::Importer
  dlload Fiddle::Handle::DEFAULT
  
  extern 'void* mmap(void*, size_t, int, int, int, long)'
  extern 'int munmap(void*, size_t)'
  
  PROT_READ = 1
  MAP_PRIVATE = 2
end

Ruby's FFI (Foreign Function Interface) enables direct memory manipulation and system calls related to virtual memory. Advanced use cases can invoke memory management functions directly, though this bypasses Ruby's memory safety mechanisms.

require 'fiddle'

# Access system memory information
class VirtualMemoryInfo
  def self.process_memory
    # Platform-specific implementation
    if RUBY_PLATFORM.include?('linux')
      # Read /proc/self/status for memory information
      status = File.read('/proc/self/status')
      vm_size = status[/VmSize:\s+(\d+)/, 1].to_i
      vm_rss = status[/VmRSS:\s+(\d+)/, 1].to_i
      
      { virtual: vm_size, resident: vm_rss }
    elsif RUBY_PLATFORM.include?('darwin')
      # Use ps command on macOS
      pid = Process.pid
      stats = `ps -o vsz,rss -p #{pid}`.lines.last.split
      { virtual: stats[0].to_i, resident: stats[1].to_i }
    end
  end
end

memory = VirtualMemoryInfo.process_memory
puts "Virtual memory: #{memory[:virtual]} KB"
puts "Resident memory: #{memory[:resident]} KB"

Copy-on-write semantics affect Ruby process forking. When a Ruby application calls Process.fork, the operating system creates a new process with a copy of the parent's virtual address space. The OS uses copy-on-write to avoid duplicating physical memory immediately. Both processes share physical pages until one writes to a page, at which point the OS creates a separate copy.

# Demonstrate copy-on-write behavior
data = Array.new(1_000_000, 'shared')

pid = fork do
  # Child process initially shares parent's memory pages
  sleep 1
  
  # Writing to shared data triggers copy-on-write
  data[0] = 'modified'
  
  # Check memory after modification
  memory = VirtualMemoryInfo.process_memory
  puts "Child RSS: #{memory[:resident]} KB"
end

# Parent continues with original data
sleep 0.5
memory = VirtualMemoryInfo.process_memory
puts "Parent RSS: #{memory[:resident]} KB"

Process.wait(pid)

Ruby's memory allocation patterns interact with virtual memory. Creating many small objects can lead to memory fragmentation within Ruby's heap, but the OS still manages the underlying page allocation. Large string or array allocations may trigger immediate page allocation, while small object allocations are satisfied from Ruby's pre-allocated pages.

# Large allocations interact directly with virtual memory
class MemoryPattern
  def allocate_large
    # Large allocation likely triggers OS page allocation
    Array.new(10_000_000, 0)
  end
  
  def allocate_small
    # Small allocations use Ruby's heap, which sits atop virtual memory
    10_000.times.map { [1, 2, 3] }
  end
  
  def measure_pattern(&block)
    before = VirtualMemoryInfo.process_memory
    result = block.call
    after = VirtualMemoryInfo.process_memory
    
    {
      virtual_delta: after[:virtual] - before[:virtual],
      resident_delta: after[:resident] - before[:resident]
    }
  end
end

pattern = MemoryPattern.new
puts "Large allocation impact:"
p pattern.measure_pattern { pattern.allocate_large }

GC.start  # Clean up before next test

puts "\nSmall allocation impact:"
p pattern.measure_pattern { pattern.allocate_small }

Performance Considerations

Virtual memory performance directly impacts application behavior. Understanding the performance characteristics and optimization techniques helps build efficient Ruby applications.

Page faults represent the most significant performance cost in virtual memory systems. Major page faults requiring disk I/O can take milliseconds, orders of magnitude slower than memory access. Applications should minimize page faults by maintaining a working set that fits in available physical memory. Ruby applications with large heaps may experience page faults during garbage collection as the collector accesses all live objects.

# Monitor page faults in Ruby processes
class PageFaultMonitor
  def self.page_faults
    if RUBY_PLATFORM.include?('linux')
      stat = File.read("/proc/#{Process.pid}/stat").split
      {
        minor_faults: stat[9].to_i,   # Page faults not requiring I/O
        major_faults: stat[11].to_i   # Page faults requiring disk I/O
      }
    end
  end
  
  def self.measure(&block)
    before = page_faults
    result = block.call
    after = page_faults
    
    {
      result: result,
      minor_faults: after[:minor_faults] - before[:minor_faults],
      major_faults: after[:major_faults] - before[:major_faults]
    }
  end
end

# Compare access patterns
result = PageFaultMonitor.measure do
  # Sequential access has better page fault characteristics
  data = Array.new(1_000_000) { |i| i }
  sum = 0
  data.each { |n| sum += n }
  sum
end

puts "Sequential access faults: #{result[:minor_faults]} minor, #{result[:major_faults]} major"

TLB (Translation Lookaside Buffer) misses occur when the hardware cache of virtual-to-physical address translations doesn't contain the needed entry. While faster than page faults, TLB misses still cost dozens to hundreds of cycles. Applications accessing memory in large, sparse patterns experience more TLB misses. Improving spatial locality—accessing nearby memory addresses—reduces TLB misses.

Memory access patterns significantly affect performance. Sequential access patterns exhibit excellent cache and TLB behavior, while random access patterns cause cache misses and TLB thrashing. Ruby's garbage collector benefits from sequential access patterns when scanning the heap.

# Compare sequential vs random access performance
require 'benchmark'

SIZE = 1_000_000
data = Array.new(SIZE) { |i| i }

Benchmark.bmbm do |x|
  x.report('sequential') do
    sum = 0
    data.each { |n| sum += n }
  end
  
  x.report('random') do
    sum = 0
    indices = (0...SIZE).to_a.shuffle
    indices.each { |i| sum += data[i] }
  end
end

# Sequential access is faster due to better memory locality

Thrashing occurs when the system spends more time paging than executing useful work. This happens when the combined working sets of active processes exceed physical memory. The system continually evicts pages that are soon accessed again, causing cascading page faults. Ruby applications can contribute to thrashing by allocating memory that exceeds available RAM.

Garbage collection interacts with virtual memory performance. During a full GC, Ruby's collector scans all live objects, potentially touching many memory pages. If the heap exceeds physical memory, this causes page faults. The compacting GC in Ruby 2.7+ can improve memory locality by moving objects together, reducing page faults in subsequent GC cycles.

# GC configuration affects virtual memory behavior
# Larger heap means more pages to scan during GC
GC::OPTS = {
  RUBY_GC_HEAP_GROWTH_FACTOR: 1.1,
  RUBY_GC_HEAP_GROWTH_MAX_SLOTS: 1_000_000,
  RUBY_GC_HEAP_INIT_SLOTS: 100_000
}

# Monitor GC impact on memory
def analyze_gc_behavior
  before_memory = VirtualMemoryInfo.process_memory
  before_gc_stat = GC.stat
  
  # Allocate and discard objects
  1000.times do
    Array.new(1000) { "data" * 100 }
  end
  
  GC.start
  
  after_memory = VirtualMemoryInfo.process_memory
  after_gc_stat = GC.stat
  
  {
    rss_change: after_memory[:resident] - before_memory[:resident],
    gc_time: after_gc_stat[:time] - before_gc_stat[:time],
    major_gc_count: after_gc_stat[:major_gc_count] - before_gc_stat[:major_gc_count]
  }
end

Huge pages reduce TLB pressure for applications with large memory footprints. A 2MB huge page covers 512 times more memory than a 4KB page with a single TLB entry. Database systems and data processing applications benefit significantly from huge pages. Ruby applications can sometimes benefit, though the Ruby runtime doesn't explicitly request huge pages. Operating system transparent huge pages may automatically use them.

Memory-mapped file performance depends on page faults and I/O patterns. Reading a memory-mapped file sequentially triggers predictable page faults that the OS can prefetch. Random access patterns cause scattered page faults with less predictable behavior. The madvise system call allows applications to hint at access patterns, though Ruby's standard library doesn't expose this functionality.

Process forking performance degrades with large memory footprints due to copy-on-write overhead. While COW avoids immediate copying, page table duplication still occurs, and writes trigger page copying. Ruby applications using forking web servers (Unicorn, Puma in clustered mode) should minimize memory modifications after forking to maximize shared memory.

# Optimize forking by loading data before fork
SHARED_DATA = File.read('shared_config.json')

# After fork, read-only access to SHARED_DATA doesn't trigger COW
pid = fork do
  # Only new data triggers page copying
  local_data = process_request(SHARED_DATA)
  write_result(local_data)
end

Tools & Ecosystem

Various tools help monitor, analyze, and optimize virtual memory usage in Ruby applications. Understanding these tools enables effective performance tuning and problem diagnosis.

Operating system monitoring tools provide real-time virtual memory statistics. The ps command shows virtual memory size (VSZ) and resident set size (RSS) for processes. The top and htop commands display memory usage for all processes with regular updates. The vmstat command reports virtual memory statistics including page faults, swapping activity, and memory allocation.

# Programmatic access to process memory statistics
class SystemMemoryStats
  def self.current_process
    pid = Process.pid
    
    if RUBY_PLATFORM.include?('linux')
      parse_linux_proc(pid)
    elsif RUBY_PLATFORM.include?('darwin')
      parse_darwin_ps(pid)
    end
  end
  
  def self.parse_linux_proc(pid)
    status = File.read("/proc/#{pid}/status")
    statm = File.read("/proc/#{pid}/statm").split.map(&:to_i)
    
    page_size = 4096  # Typical page size in bytes
    
    {
      virtual_memory_kb: statm[0] * page_size / 1024,
      resident_set_kb: statm[1] * page_size / 1024,
      shared_pages: statm[2],
      text_segment_kb: statm[3] * page_size / 1024,
      data_segment_kb: statm[5] * page_size / 1024
    }
  end
  
  def self.parse_darwin_ps(pid)
    output = `ps -o vsz,rss,tsiz,dsiz -p #{pid}`.lines.last.split
    {
      virtual_memory_kb: output[0].to_i,
      resident_set_kb: output[1].to_i,
      text_segment_kb: output[2].to_i,
      data_segment_kb: output[3].to_i
    }
  end
end

# Monitor memory over time
def track_memory_usage(duration_seconds, interval_seconds)
  measurements = []
  end_time = Time.now + duration_seconds
  
  while Time.now < end_time
    measurements << {
      timestamp: Time.now,
      stats: SystemMemoryStats.current_process
    }
    sleep interval_seconds
  end
  
  measurements
end

Ruby-specific memory profiling gems provide detailed analysis of object allocation and retention. The memory_profiler gem tracks allocations during code execution, showing which classes allocate the most memory and where in the code allocation occurs. The derailed_benchmarks gem analyzes memory usage in Rails applications.

# Memory profiling with memory_profiler gem
require 'memory_profiler'

report = MemoryProfiler.report do
  # Code to profile
  data = []
  10_000.times do |i|
    data << { id: i, value: "item_#{i}" }
  end
  
  # Process data
  processed = data.map { |item| item[:value].upcase }
end

# Analyze allocations
report.pretty_print(scale_bytes: true)

The objspace module in Ruby's standard library provides insights into object memory layout and garbage collector behavior. Methods like ObjectSpace.memsize_of report memory consumed by individual objects. The ObjectSpace.trace_object_allocations mechanism tracks where objects are created.

require 'objspace'

# Track object allocations
ObjectSpace.trace_object_allocations_start

def create_data_structure
  {
    users: Array.new(100) { |i| { id: i, name: "User #{i}" } },
    cache: Hash.new { |h, k| h[k] = [] }
  }
end

data = create_data_structure

# Analyze allocation sites
ObjectSpace.trace_object_allocations_stop

objects = ObjectSpace.each_object.select { |o| o.is_a?(Hash) || o.is_a?(Array) }
by_file = objects.group_by { |o| ObjectSpace.allocation_sourcefile(o) }

by_file.each do |file, objs|
  next if file.nil?
  total_size = objs.sum { |o| ObjectSpace.memsize_of(o) }
  puts "#{file}: #{objs.count} objects, #{total_size} bytes"
end

System-level profilers like perf (Linux) and Instruments (macOS) can analyze page fault behavior and TLB misses. These tools require kernel-level access but provide detailed performance counters showing how applications interact with virtual memory.

Application Performance Monitoring (APM) tools like New Relic, DataDog, and Scout track memory usage over time in production environments. These tools correlate memory usage with application behavior, helping identify memory leaks and optimization opportunities.

The get_process_mem gem provides a simple interface for monitoring Ruby process memory usage across platforms. It normalizes the differences between operating systems, reporting both virtual memory and RSS.

require 'get_process_mem'

mem = GetProcessMem.new
puts "Virtual memory: #{mem.mb} MB"
puts "Resident set: #{mem.kb} KB"

# Track memory growth over time
initial_mem = mem.bytes
perform_operations
final_mem = mem.bytes
growth = final_mem - initial_mem

puts "Memory growth: #{growth / 1024} KB"

Container orchestration platforms like Kubernetes expose memory metrics and enforce limits. Ruby applications running in containers should monitor memory usage to avoid exceeding container limits, which triggers OOM (out-of-memory) kills.

Common Pitfalls

Several common mistakes and misconceptions about virtual memory lead to performance problems and unexpected behavior in Ruby applications.

Confusing virtual memory size with actual memory usage causes misdiagnosis of memory problems. Virtual memory size (VSZ) represents the total address space allocated to a process, including memory-mapped files and reserved but uncommitted pages. RSS (Resident Set Size) represents actual physical memory in use. A large VSZ doesn't necessarily indicate a problem if RSS remains reasonable.

# Demonstrate VSZ vs RSS difference
def memory_snapshot
  mem = SystemMemoryStats.current_process
  puts "Virtual memory: #{mem[:virtual_memory_kb]} KB"
  puts "Resident set: #{mem[:resident_set_kb]} KB"
  puts "Ratio: #{(mem[:virtual_memory_kb].to_f / mem[:resident_set_kb]).round(2)}x"
end

memory_snapshot

# Memory-mapped files increase VSZ significantly
File.open('large_file.dat', 'r') do |f|
  f.read(1000)  # Read small portion
  memory_snapshot  # VSZ increases, RSS increases minimally
end

Memory fragmentation within Ruby's heap differs from memory fragmentation at the OS level. Ruby's GC can leave gaps in its heap as objects are freed, but the underlying pages remain allocated. This means RSS stays high even after objects are garbage collected. Ruby 2.7+ includes compaction to address this issue.

# Demonstrate heap fragmentation
def create_fragmentation
  # Allocate many objects
  objects = 100_000.times.map { |i| "Object #{i}" * 10 }
  
  before_rss = SystemMemoryStats.current_process[:resident_set_kb]
  
  # Free most objects, keeping some alive
  kept = objects.select { |o| o.hash % 100 == 0 }
  objects = nil
  
  GC.start
  after_rss = SystemMemoryStats.current_process[:resident_set_kb]
  
  puts "RSS reduction: #{before_rss - after_rss} KB"
  puts "Retained objects: #{kept.count}"
  
  # RSS may not decrease proportionally to freed objects
  # due to heap fragmentation
end

Assuming garbage collection immediately frees memory overlooks the interaction between Ruby's GC and virtual memory. When Ruby's GC frees objects, it returns pages to its heap pool but may not return them to the operating system. The RSS remains unchanged until memory pressure causes Ruby to release pages.

Ignoring copy-on-write semantics in forking applications wastes memory. Modifying large data structures after forking triggers page copying, eliminating the memory sharing benefit. Pre-fork servers should load all shared data before forking and avoid writing to shared structures.

# Bad: Modifying shared data after fork
SHARED_CONFIG = { setting1: 'value1', setting2: 'value2' }

fork do
  # This modification triggers copy-on-write
  SHARED_CONFIG[:worker_id] = Process.pid  # Copies the page
  process_requests
end

# Good: Use separate data structures for worker-specific state
SHARED_CONFIG = { setting1: 'value1', setting2: 'value2' }.freeze
WORKER_STATE = {}

fork do
  # Modifications affect only local structure
  WORKER_STATE[:worker_id] = Process.pid
  process_requests
end

Allocating memory beyond physical RAM causes thrashing. Ruby applications that allocate gigabytes of data on systems with limited RAM experience severe performance degradation as the system constantly pages memory to disk. Monitoring RSS and page fault rates helps identify this condition.

Memory leaks accumulate memory that's never freed, eventually exhausting available memory. In Ruby, memory leaks often result from retaining references to objects in long-lived collections. While Ruby's GC prevents traditional memory leaks, logical leaks occur when objects remain reachable but unused.

# Memory leak example: accumulating references
class CachingService
  def initialize
    @cache = {}
  end
  
  def process(key, data)
    # Leak: cache grows unbounded
    @cache[key] = data.dup
    compute_result(data)
  end
end

# Fix: implement cache eviction
class BoundedCachingService
  MAX_CACHE_SIZE = 10_000
  
  def initialize
    @cache = {}
    @access_order = []
  end
  
  def process(key, data)
    if @cache.size >= MAX_CACHE_SIZE
      # Evict least recently used entry
      evicted_key = @access_order.shift
      @cache.delete(evicted_key)
    end
    
    @cache[key] = data.dup
    @access_order << key
    compute_result(data)
  end
end

Failing to monitor page faults prevents identifying memory access bottlenecks. High major page fault rates indicate memory pressure or inefficient access patterns. Minor page faults during startup are normal, but ongoing minor faults during steady-state operation suggest working set issues.

Overlooking platform differences in virtual memory behavior causes portability problems. Linux, macOS, and BSD systems implement virtual memory differently. Memory overcommit policies, page cache behavior, and memory reclamation strategies vary across operating systems.

Reference

Virtual Memory Components

Component Description Purpose
Virtual Address Space Range of memory addresses available to a process Provides isolation and abstraction from physical memory
Page Table Data structure mapping virtual pages to physical frames Enables address translation and memory protection
Translation Lookaside Buffer Hardware cache of address translations Accelerates virtual to physical address translation
Memory Management Unit Hardware component performing address translation Enforces memory protection and performs address mapping
Page Frame Fixed-size block of physical memory Basic unit of physical memory allocation
Page Fault Handler OS routine handling page fault exceptions Loads pages from disk and updates page tables
Working Set Collection of pages actively used by a process Determines memory requirements and page replacement

Page Table Entry Flags

Flag Purpose Effect
Present Indicates if page is in physical memory Page fault if not set
Writable Allows write access to page Protection fault if write attempted without flag
User Allows user-mode access Protection fault if user mode accesses without flag
Accessed Tracks page access Used by page replacement algorithms
Dirty Indicates page has been modified Determines if page must be written to disk
Executable Allows instruction execution from page Protection fault if execution attempted without flag

Memory Statistics

Metric Meaning Interpretation
VSZ Virtual memory size in use Total address space allocated
RSS Resident set size Physical memory currently in use
Minor Page Fault Page fault not requiring disk I/O Page in memory but not in page table
Major Page Fault Page fault requiring disk read Page must be loaded from disk
TLB Miss Translation not in TLB cache Requires page table walk
Page In Page loaded from disk to memory Indicates memory pressure or sequential access
Page Out Page written from memory to disk Indicates memory pressure

GC and Memory Interaction

GC Setting Impact on Virtual Memory Recommendation
RUBY_GC_HEAP_INIT_SLOTS Initial heap allocation Set based on application size to reduce early allocations
RUBY_GC_HEAP_GROWTH_FACTOR Rate of heap expansion Lower values reduce memory spikes
RUBY_GC_HEAP_GROWTH_MAX_SLOTS Maximum heap growth per expansion Limits memory allocation bursts
RUBY_GC_MALLOC_LIMIT Threshold for malloc-based GC trigger Affects native memory management

Ruby Memory Monitoring Methods

Method/Tool Information Provided Use Case
GC.stat Garbage collector statistics Monitor GC behavior and heap size
ObjectSpace.memsize_of Memory size of specific object Identify large objects
ObjectSpace.count_objects Count of objects by class Track object proliferation
get_process_mem gem Process memory statistics Simple cross-platform memory monitoring
memory_profiler gem Detailed allocation tracking Identify allocation hotspots
/proc/pid/status Linux process memory details Detailed memory breakdown on Linux

Common Page Sizes

Size Use Case Advantage Disadvantage
4 KB Default page size Flexible, minimal internal fragmentation Higher TLB pressure
2 MB Huge pages Reduced TLB misses, lower page table overhead Increased internal fragmentation
1 GB Super huge pages Maximum TLB efficiency Very high internal fragmentation

Copy-on-Write Scenarios

Scenario Behavior Optimization
Fork without modification Pages shared between parent and child Minimal memory overhead
Fork with writes Pages copied on first write Minimize post-fork writes
Preload then fork Shared data in both processes Load read-only data before forking
Fork then exec Pages discarded immediately COW overhead wasted