Overview
Ruby's garbage collection compaction reduces memory fragmentation by moving live objects to eliminate gaps in the heap. This process consolidates allocated memory regions and creates larger contiguous free spaces. The compactor runs during major garbage collection cycles and relocates objects to minimize heap fragmentation.
The GC
module provides the primary interface for compaction operations through methods like GC.compact
and GC.auto_compact
. When compaction occurs, Ruby updates all references to moved objects automatically, maintaining program correctness while optimizing memory layout.
Compaction addresses fragmentation that accumulates as objects are allocated and freed. Without compaction, the heap develops scattered free regions that cannot satisfy larger allocation requests, leading to unnecessary heap growth.
# Check current compaction status
GC.stat(:compact_count)
# => 0
# Trigger manual compaction
GC.compact
# => {:considered=>12845, :moved=>8234}
# Verify compaction occurred
GC.stat(:compact_count)
# => 1
The compactor operates on object generations, typically focusing on older objects that are less likely to be freed soon. This generational approach minimizes the performance impact while achieving significant memory layout improvements.
# Enable automatic compaction
GC.auto_compact = true
# Configure compaction timing
GC.auto_compact = true
major_gc_count = GC.stat[:major_gc_count]
GC.start(full_mark: true)
# Compaction may occur during this major GC
Basic Usage
Manual compaction provides direct control over when defragmentation occurs. The GC.compact
method performs immediate compaction and returns statistics about the operation.
# Basic compaction execution
result = GC.compact
puts "Objects considered: #{result[:considered]}"
puts "Objects moved: #{result[:moved]}"
# => Objects considered: 15432
# => Objects moved: 9876
Automatic compaction eliminates the need for manual triggering by integrating compaction into regular garbage collection cycles. Enable automatic mode through GC.auto_compact
.
# Enable automatic compaction
GC.auto_compact = true
# Check current setting
puts "Auto compaction: #{GC.auto_compact}"
# => Auto compaction: true
# Disable when not needed
GC.auto_compact = false
The compaction process reports detailed statistics through the returned hash. The :considered
key indicates total objects evaluated for movement, while :moved
shows objects actually relocated.
def analyze_compaction
before_stats = GC.stat
compact_result = GC.compact
after_stats = GC.stat
{
objects_moved: compact_result[:moved],
heap_pages_before: before_stats[:heap_sorted_length],
heap_pages_after: after_stats[:heap_sorted_length],
compaction_time: measure_compaction_time
}
end
stats = analyze_compaction
puts "Moved #{stats[:objects_moved]} objects"
puts "Heap pages: #{stats[:heap_pages_before]} → #{stats[:heap_pages_after]}"
Compaction works with all object types except those explicitly pinned by the virtual machine. Most user-defined objects participate in compaction automatically without special handling.
class DataProcessor
def initialize
@buffer = Array.new(10000) { rand(1000) }
@cache = {}
end
def compact_and_process
# Objects in @buffer and @cache can be moved
GC.compact
process_data
end
end
processor = DataProcessor.new
# All instance variables participate in compaction
processor.compact_and_process
The timing of compaction affects application performance. Manual compaction during low-activity periods minimizes user impact, while automatic compaction distributes the overhead across normal operations.
Performance & Memory
Compaction reduces memory usage by eliminating fragmentation holes that prevent efficient allocation. Applications with high object churn see the largest improvements as frequent allocations and deallocations create scattered free regions.
def measure_memory_impact
# Create fragmentation
objects = Array.new(50000) { Object.new }
objects.select!.with_index { |_, i| i.even? }
before_memory = GC.stat(:heap_allocated_pages)
before_free = GC.stat(:heap_free_slots)
GC.compact
after_memory = GC.stat(:heap_allocated_pages)
after_free = GC.stat(:heap_free_slots)
{
pages_freed: before_memory - after_memory,
slots_consolidated: after_free - before_free
}
end
impact = measure_memory_impact
puts "Pages freed: #{impact[:pages_freed]}"
puts "Slots consolidated: #{impact[:slots_consolidated]}"
Compaction overhead varies based on object count and reference density. Applications with many interconnected objects experience higher compaction costs as the collector must update more references.
require 'benchmark'
class ReferenceHeavyStructure
def initialize(size)
@nodes = Array.new(size) { { data: rand(1000), refs: [] } }
@nodes.each_with_index do |node, i|
# Create cross-references
refs = (0...size).to_a.sample(10)
node[:refs] = refs.map { |idx| @nodes[idx] }
end
end
end
# Measure compaction time with different reference densities
sizes = [1000, 5000, 10000]
sizes.each do |size|
structure = ReferenceHeavyStructure.new(size)
time = Benchmark.realtime { GC.compact }
puts "#{size} objects: #{(time * 1000).round(2)}ms"
end
# => 1000 objects: 12.34ms
# => 5000 objects: 89.67ms
# => 10000 objects: 245.12ms
Memory layout improvements from compaction enable more efficient allocation patterns. Consolidated free space allows larger objects to be allocated without growing the heap.
def demonstrate_allocation_efficiency
# Create fragmentation pattern
small_objects = Array.new(100000) { "x" * 100 }
small_objects.select!.with_index { |_, i| i % 3 == 0 }
# Attempt large allocation before compaction
begin
large_object = "x" * 1_000_000
puts "Large allocation succeeded without compaction"
rescue NoMemoryError
puts "Large allocation failed - heap fragmented"
end
GC.compact
# Try large allocation after compaction
large_object = "x" * 1_000_000
puts "Large allocation succeeded after compaction"
end
The frequency of compaction affects overall application performance. Too frequent compaction wastes CPU cycles, while infrequent compaction allows fragmentation to accumulate.
class CompactionScheduler
def initialize(threshold: 100)
@threshold = threshold
@last_compact_count = GC.stat(:major_gc_count)
end
def should_compact?
current_count = GC.stat(:major_gc_count)
return false if current_count - @last_compact_count < @threshold
# Check fragmentation level
total_slots = GC.stat(:heap_allocated_pages) * GC.stat(:heap_page_length)
used_slots = GC.stat(:heap_live_slots)
fragmentation = 1.0 - (used_slots.to_f / total_slots)
fragmentation > 0.3 # Compact if >30% fragmented
end
def compact_if_needed
return unless should_compact?
result = GC.compact
@last_compact_count = GC.stat(:major_gc_count)
result
end
end
Error Handling & Debugging
Compaction failures are rare but can occur when the heap reaches critical memory limits. The compactor may skip moving objects if insufficient free space exists for temporary storage during the move process.
def safe_compaction_with_retry
attempts = 0
max_attempts = 3
begin
result = GC.compact
puts "Compaction successful: #{result[:moved]} objects moved"
result
rescue NoMemoryError => e
attempts += 1
if attempts < max_attempts
puts "Compaction failed, retrying (attempt #{attempts})"
# Force full GC to free memory for compaction
GC.start(full_mark: true)
retry
else
puts "Compaction failed after #{max_attempts} attempts: #{e.message}"
raise
end
end
end
safe_compaction_with_retry
Debugging compaction issues requires monitoring heap statistics before and after operations. Changes in object counts and memory usage reveal compaction effectiveness.
class CompactionDebugger
def self.debug_compaction(&block)
before_stats = GC.stat.dup
before_time = Time.now
result = yield
after_time = Time.now
after_stats = GC.stat.dup
report = {
duration: (after_time - before_time) * 1000,
objects_moved: result[:moved],
objects_considered: result[:considered],
move_ratio: result[:moved].to_f / result[:considered],
heap_pages_before: before_stats[:heap_allocated_pages],
heap_pages_after: after_stats[:heap_allocated_pages],
pages_freed: before_stats[:heap_allocated_pages] - after_stats[:heap_allocated_pages]
}
puts "=== Compaction Debug Report ==="
report.each { |key, value| puts "#{key}: #{value}" }
puts "==============================="
result
end
end
# Usage with debugging
CompactionDebugger.debug_compaction { GC.compact }
Object pinning prevents certain objects from being moved during compaction. This occurs with objects that have references from C extensions or are otherwise locked by the virtual machine.
def analyze_pinned_objects
# Create mix of pinnable and movable objects
movable_objects = Array.new(1000) { Object.new }
# Some objects may be pinned by VM internals
result = GC.compact
pinned_count = result[:considered] - result[:moved]
pin_ratio = pinned_count.to_f / result[:considered]
if pin_ratio > 0.1
puts "High pin ratio detected: #{(pin_ratio * 100).round(1)}%"
puts "Consider reviewing C extensions or native library usage"
end
{
pinned_objects: pinned_count,
pin_ratio: pin_ratio,
movable_objects: result[:moved]
}
end
pinning_analysis = analyze_pinned_objects
Memory pressure during compaction can cause performance degradation. Monitor system memory usage to detect when compaction occurs under constrained conditions.
require 'objspace'
def memory_pressure_check
gc_stats = GC.stat
heap_usage = gc_stats[:heap_allocated_pages] * gc_stats[:heap_page_length]
live_objects = gc_stats[:heap_live_slots]
pressure_indicators = {
high_fragmentation: (heap_usage - live_objects) > (heap_usage * 0.4),
frequent_gc: gc_stats[:major_gc_count] > 1000,
large_heap: gc_stats[:heap_allocated_pages] > 10000
}
if pressure_indicators.any? { |_, condition| condition }
puts "Memory pressure detected:"
pressure_indicators.each do |indicator, present|
puts " #{indicator}: #{present}" if present
end
puts "Consider compaction to reduce pressure"
return true
end
false
end
if memory_pressure_check
GC.compact
end
Production Patterns
Production applications benefit from scheduled compaction during low-traffic periods. This approach minimizes user-facing performance impact while maintaining optimal memory layout.
class ProductionCompactionManager
def initialize
@last_compaction = Time.now
@compaction_interval = 3600 # 1 hour
@min_fragmentation = 0.25
end
def should_compact_now?
return false unless low_traffic_period?
return false if Time.now - @last_compaction < @compaction_interval
current_fragmentation > @min_fragmentation
end
def compact_if_appropriate
return unless should_compact_now?
start_time = Time.now
result = GC.compact
duration = Time.now - start_time
log_compaction_result(result, duration)
@last_compaction = Time.now
result
end
private
def low_traffic_period?
# Implement based on application patterns
hour = Time.now.hour
(2..6).include?(hour) # 2 AM - 6 AM
end
def current_fragmentation
stats = GC.stat
total_slots = stats[:heap_allocated_pages] * stats[:heap_page_length]
used_slots = stats[:heap_live_slots]
1.0 - (used_slots.to_f / total_slots)
end
def log_compaction_result(result, duration)
puts "[COMPACTION] Moved #{result[:moved]} objects in #{duration.round(3)}s"
end
end
# Integrate with application lifecycle
compaction_manager = ProductionCompactionManager.new
Thread.new do
loop do
compaction_manager.compact_if_appropriate
sleep 300 # Check every 5 minutes
end
end
Web applications can trigger compaction between request cycles to avoid impacting response times. Integration with application servers enables strategic timing.
# Rack middleware for compaction
class CompactionMiddleware
def initialize(app, options = {})
@app = app
@request_count = 0
@compact_interval = options[:interval] || 1000
end
def call(env)
@request_count += 1
response = @app.call(env)
if should_compact?
Thread.new { perform_background_compaction }
end
response
end
private
def should_compact?
@request_count % @compact_interval == 0
end
def perform_background_compaction
start_time = Time.now
result = GC.compact
duration = Time.now - start_time
Rails.logger.info "Background compaction: #{result[:moved]} objects moved in #{duration.round(3)}s"
rescue => e
Rails.logger.error "Compaction failed: #{e.message}"
end
end
# Rails application setup
Rails.application.config.middleware.use CompactionMiddleware, interval: 500
Background job processing provides natural compaction opportunities between job executions. This pattern works well with systems like Sidekiq or DelayedJob.
class CompactionAwareWorker
include Sidekiq::Worker
def perform(*args)
# Execute the actual work
result = process_job(*args)
# Compact after processing if conditions are met
compact_if_beneficial
result
end
private
def compact_if_beneficial
return unless should_compact_after_job?
# Perform compaction in background to avoid blocking job queue
Thread.new do
begin
compaction_result = GC.compact
log_compaction_success(compaction_result)
rescue => e
log_compaction_error(e)
end
end
end
def should_compact_after_job?
# Check memory fragmentation level
stats = GC.stat
fragmentation = calculate_fragmentation(stats)
# Compact if fragmentation exceeds threshold and sufficient time has passed
fragmentation > 0.3 && time_since_last_compaction > 300
end
def calculate_fragmentation(stats)
total = stats[:heap_allocated_pages] * stats[:heap_page_length]
used = stats[:heap_live_slots]
(total - used).to_f / total
end
def time_since_last_compaction
Time.now.to_i - (Rails.cache.read('last_compaction_time') || 0)
end
def log_compaction_success(result)
Rails.cache.write('last_compaction_time', Time.now.to_i)
Sidekiq.logger.info "Compacted #{result[:moved]} objects"
end
def log_compaction_error(error)
Sidekiq.logger.error "Compaction failed: #{error.message}"
end
end
Common Pitfalls
Excessive compaction frequency wastes CPU cycles without providing proportional benefits. Applications that compact too often see diminishing returns as objects have insufficient time to create meaningful fragmentation.
# Problematic: Compacting after every GC
class OvereagerCompactor
def initialize
# BAD: This will compact constantly
ObjectSpace.define_finalizer(Object.new) { GC.compact }
end
end
# Better: Threshold-based compaction
class ThresholdCompactor
def initialize
@gc_count_at_last_compaction = GC.stat[:major_gc_count]
@min_gc_interval = 50
end
def compact_if_due
current_gc_count = GC.stat[:major_gc_count]
return unless current_gc_count - @gc_count_at_last_compaction > @min_gc_interval
GC.compact
@gc_count_at_last_compaction = current_gc_count
end
end
Measuring compaction benefits incorrectly leads to wrong optimization decisions. Simply counting moved objects doesn't indicate actual memory improvements.
# Misleading measurement
def bad_compaction_measurement
result = GC.compact
puts "Compaction moved #{result[:moved]} objects - SUCCESS!"
# This doesn't tell us if memory usage actually improved
end
# Proper measurement
def accurate_compaction_measurement
before_pages = GC.stat[:heap_allocated_pages]
before_fragmentation = calculate_fragmentation
result = GC.compact
after_pages = GC.stat[:heap_allocated_pages]
after_fragmentation = calculate_fragmentation
improvement = {
pages_freed: before_pages - after_pages,
fragmentation_reduction: before_fragmentation - after_fragmentation,
objects_moved: result[:moved]
}
if improvement[:fragmentation_reduction] > 0.05
puts "Meaningful compaction: #{improvement[:fragmentation_reduction] * 100}% less fragmented"
else
puts "Minimal compaction benefit - consider reducing frequency"
end
end
def calculate_fragmentation
stats = GC.stat
total = stats[:heap_allocated_pages] * stats[:heap_page_length]
used = stats[:heap_live_slots]
(total - used).to_f / total
end
Ignoring compaction timing can degrade application performance. Compacting during peak usage periods blocks request processing and increases latency.
# Problematic: Synchronous compaction in request handler
def bad_request_handler(request)
process_request(request)
GC.compact # BLOCKS the request thread!
send_response
end
# Better: Asynchronous compaction
def good_request_handler(request)
process_request(request)
# Compact in background if needed
if should_compact?
Thread.new { GC.compact }
end
send_response
end
def should_compact?
@last_compact ||= Time.now
return false if Time.now - @last_compact < 600 # Min 10 minutes between compactions
stats = GC.stat
fragmentation = 1.0 - (stats[:heap_live_slots].to_f /
(stats[:heap_allocated_pages] * stats[:heap_page_length]))
if fragmentation > 0.4
@last_compact = Time.now
true
else
false
end
end
Expecting compaction to solve all memory problems leads to disappointment. Compaction addresses fragmentation but cannot fix memory leaks or excessive object retention.
# Compaction won't fix this memory leak
class MemoryLeaker
@@global_cache = []
def process_data(data)
result = expensive_computation(data)
@@global_cache << result # Objects never freed!
# Compaction won't help - objects are still referenced
GC.compact if @@global_cache.size % 1000 == 0
result
end
end
# Fix the underlying issue instead
class ProperMemoryManagement
def initialize(cache_limit: 10000)
@cache = {}
@cache_limit = cache_limit
end
def process_data(data)
cache_key = data.hash
return @cache[cache_key] if @cache.key?(cache_key)
result = expensive_computation(data)
# Proper cache management
if @cache.size >= @cache_limit
old_key = @cache.keys.first
@cache.delete(old_key)
end
@cache[cache_key] = result
result
end
end
Automatic compaction settings can conflict with application-specific memory patterns. Blindly enabling automatic compaction without understanding the workload may hurt performance.
# Monitor automatic compaction impact
class AutoCompactionMonitor
def self.evaluate_auto_compaction
# Test with auto compaction disabled
GC.auto_compact = false
baseline_metrics = run_workload_sample
# Test with auto compaction enabled
GC.auto_compact = true
auto_compact_metrics = run_workload_sample
comparison = {
throughput_change: (auto_compact_metrics[:throughput] / baseline_metrics[:throughput] - 1) * 100,
memory_change: (auto_compact_metrics[:memory] / baseline_metrics[:memory] - 1) * 100,
gc_time_change: (auto_compact_metrics[:gc_time] / baseline_metrics[:gc_time] - 1) * 100
}
recommendation = if comparison[:throughput_change] < -5
"Disable auto compaction - significant throughput loss"
elsif comparison[:memory_change] < -10
"Enable auto compaction - significant memory savings"
else
"Manual compaction may be better - mixed results"
end
{ comparison: comparison, recommendation: recommendation }
end
def self.run_workload_sample
# Implement workload representative of production
start_time = Time.now
start_memory = GC.stat[:heap_allocated_pages]
# Simulate typical application work
1000.times { create_typical_objects }
{
throughput: 1000 / (Time.now - start_time),
memory: GC.stat[:heap_allocated_pages] - start_memory,
gc_time: GC.stat[:total_time]
}
end
end
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
GC.compact |
None | Hash |
Performs immediate compaction, returns statistics |
GC.auto_compact |
None | Boolean |
Returns current automatic compaction setting |
GC.auto_compact= |
enabled (Boolean) |
Boolean |
Enables or disables automatic compaction |
GC.verify_compaction_references |
toward: :empty (Symbol) |
Hash |
Verifies reference consistency after compaction |
Compaction Statistics
The hash returned by GC.compact
contains:
Key | Type | Description |
---|---|---|
:considered |
Integer |
Total objects evaluated for movement |
:moved |
Integer |
Objects successfully relocated |
GC Statistics Related to Compaction
Statistic | Description |
---|---|
:compact_count |
Number of compactions performed |
:heap_allocated_pages |
Total heap pages allocated |
:heap_sorted_length |
Length of sorted heap page list |
:heap_free_slots |
Available object slots in heap |
:heap_live_slots |
Slots containing live objects |
:heap_page_length |
Objects per heap page |
Configuration Options
Setting | Type | Default | Description |
---|---|---|---|
Auto compaction | Boolean | false |
Automatic compaction during major GC |
Common Fragmentation Calculation
def heap_fragmentation_ratio
stats = GC.stat
total_slots = stats[:heap_allocated_pages] * stats[:heap_page_length]
used_slots = stats[:heap_live_slots]
free_slots = total_slots - used_slots
free_slots.to_f / total_slots
end
Compaction Timing Guidelines
Fragmentation Level | Recommended Action |
---|---|
< 20% | No compaction needed |
20-30% | Consider compaction during low activity |
30-50% | Schedule compaction soon |
> 50% | Urgent compaction recommended |
Performance Impact Estimates
Objects Count | Typical Compaction Time |
---|---|
< 10,000 | 1-5 milliseconds |
10,000-100,000 | 5-50 milliseconds |
100,000-1,000,000 | 50-500 milliseconds |
> 1,000,000 | 500+ milliseconds |
Error Conditions
Error | Cause | Solution |
---|---|---|
NoMemoryError during compaction |
Insufficient heap space for object movement | Free memory before compacting |
High pinned object ratio | Many objects locked by C extensions | Review native library usage |
Compaction provides no benefit | Low fragmentation or recent compaction | Reduce compaction frequency |