Deep Dive Into Garbage Collection in Ruby 3: Compaction

A Guide to Ruby 3 Compaction: Unleashing the Power of GC.compact

Patrick Karsh
5 min readApr 5, 2023

In Ruby 3, compaction is a garbage collection feature that aims to reduce memory fragmentation, improve cache locality, and potentially increase application performance. Memory fragmentation occurs when allocated memory blocks are interspersed with small gaps or holes, making it harder for the memory manager to allocate larger continuous memory blocks. Fragmentation can cause the application to request more memory from the system than it actually needs, leading to increased memory usage and reduced performance.

Heap compaction in Ruby 3 works by rearranging the memory layout of live objects in the heap, moving them closer together, and consolidating the freed memory. This process has several benefits:

Reduced memory fragmentation: Compaction moves live objects closer together, reducing fragmentation in the heap and making it easier for the memory manager to allocate continuous memory blocks more efficiently.

Improved cache locality: Compaction can improve cache locality by ensuring that frequently accessed objects are stored closer together in memory. This can reduce the number of cache misses, leading to better performance for memory-bound applications.

Simplified memory management: Compaction reduces wasted memory due to fragmentation, potentially decreasing the overall memory footprint of the application.

Faster garbage collection: Compaction can lead to faster garbage collection cycles, as the garbage collector has to scan through fewer memory blocks when searching for dead objects.

Ruby 3.0 introduces the GC.compact method, which allows you to perform heap compaction manually in your Ruby application. By calling GC.compact, the Ruby garbage collector will compact the heap, moving live objects closer together and freeing up memory.

It’s important to note that the compaction process itself can introduce some overhead, as objects need to be moved and references updated. However, the benefits of reduced

Example

# Define a simple class
class Foo
attr_accessor :data

def initialize(size)
@data = Array.new(size) { rand(1..100) }
end
end

# Create an array to store objects
objects = []

# Create a large number of objects and store them in the array
10_000.times do
objects << Foo.new(100)
end

# Simulate deallocating some objects by removing them from the array
objects = objects.slice(0, 5_000)

# Print memory usage statistics before compaction
puts "Before GC.compact:"
puts "Total allocated slots: #{GC.stat(:total_allocated_slots)}"
puts "Free slots: #{GC.stat(:free_slots)}"

# Call GC.compact to compact the heap
GC.compact

# Print memory usage statistics after compaction
puts "\nAfter GC.compact:"
puts "Total allocated slots: #{GC.stat(:total_allocated_slots)}"
puts "Free slots: #{GC.stat(:free_slots)}"

In this example, we create a large number of Foo objects, which consume memory. After deallocating some of the objects by removing them from the array, we call GC.compact to compact the heap. The script prints memory usage statistics before and after the compaction, illustrating the impact of using GC.compact on memory fragmentation and usage. Remember that the benefits of GC.compact will depend on your specific application.

At what point in a long running task should you call GC.compact?

Deciding when to call GC.compact in a long-running task depends on the nature of the task and the memory usage patterns of your application. There isn't a one-size-fits-all answer, but here are some guidelines to help you decide when to call GC.compact:

After processing large data sets or completing memory-intensive operations: If your long-running task processes large data sets or performs memory-intensive operations, it’s a good idea to call GC.compact after completing those operations. Compacting the heap at this point can help consolidate the freed memory and make it available for future allocations more efficiently.

After deallocating a significant number of objects: If your task deallocates many objects at once or in a short period, you might want to call GC.compact afterward to reduce memory fragmentation caused by the deallocations.

During periods of low application activity: If your long-running task has periods of low activity or idle time, consider compacting the heap during these periods. This can help minimize the impact of the compaction process on the application’s performance and responsiveness.

Periodically, at regular intervals: If your task’s memory usage patterns are less predictable, you can call GC.compact periodically, at regular intervals. This approach can help ensure that the heap is compacted and memory fragmentation is reduced over time. Be cautious not to call GC.compact too frequently, as it can introduce unnecessary overhead.

When memory usage exceeds a certain threshold: Monitor your application’s memory usage, and call GC.compact when memory usage exceeds a predefined threshold. This can help prevent out-of-memory errors and ensure that memory is being used efficiently.

To determine the best approach for your specific application, monitor and profile your application’s performance and memory usage. Analyze the impact of GC.compact on memory fragmentation, overall memory usage, and application performance. Based on this information, you can decide when and how often to call GC.compact in your long-running task to optimize memory usage and performance.

What is the default type of Garbage Collection in Ruby 3?

In Ruby 3.0, the default garbage collection mode is a combination of generational and incremental garbage collection techniques. This mode builds upon the improvements introduced in earlier Ruby versions to provide efficient memory management and improved performance.

Generational Garbage Collection

Ruby 3.0 uses a generational garbage collection strategy, which was first introduced in Ruby 2.1. In this approach, objects are divided into two generations: young and old. New objects are allocated in the young generation, and objects that survive multiple garbage collection cycles are promoted to the old generation. Generational garbage collection allows the garbage collector to focus on the young generation, where most dead objects are expected to be, leading to improved GC performance.

Incremental Garbage Collection

Ruby 3.0 also incorporates incremental garbage collection, which was introduced in Ruby 2.2. Incremental garbage collection reduces the pause times associated with garbage collection by splitting the garbage collection process into smaller steps. This allows the application to continue executing while garbage collection is performed incrementally, minimizing the impact of garbage collection on application responsiveness.

The combination of generational and incremental garbage collection techniques in Ruby 3.0 provides efficient memory management and improved performance for Ruby applications. Additionally, Ruby 3.0 also includes other garbage collection features, such as heap compaction, which can be manually triggered using the GC.compact method.

--

--

Patrick Karsh
Patrick Karsh

Written by Patrick Karsh

NYC-based Ruby on Rails and Javascript Engineer leveraging AI to explore Engineering. https://linktr.ee/patrickkarsh

No responses yet