GC.Stat Mini Series Part 1: Counts
Hi!
If you’ve ever run GC.stat
before, you’ve likely seen output that looks something like this:
{ :count=>3025, :heap_allocated_pages=>347, :heap_sorted_length=>4239, :heap_allocatable_pages=>1, :heap_available_slots=>141784, :heap_live_slots=>71343, :heap_free_slots=>70441, :heap_final_slots=>0, :heap_marked_slots=>49297, :heap_eden_pages=>346, :heap_tomb_pages=>1, :total_allocated_pages=>4239, :total_freed_pages=>3892, :total_allocated_objects=>153587485, :total_freed_objects=>153516142, :malloc_increase_bytes=>191488, :malloc_increase_bytes_limit=>16777216, :minor_gc_count=>2993, :major_gc_count=>32, :compact_count=>11, :read_barrier_faults=>0, :total_moved_objects=>11025, :remembered_wb_unprotected_objects=>216, :remembered_wb_unprotected_objects_limit=>432, :old_objects=>49065, :old_objects_limit=>98130, :oldmalloc_increase_bytes=>191488, :oldmalloc_increase_bytes_limit=>18384822 }
Ruby exposes so much information about what its garbage collector has done through GC.stat
. I began a blog post describing each key / value pair in this hash, and quickly realized that even the most patient reader might not make it through such a dense post.
With this in mind, I’ve instead decided to experiment with chunking this information into a mini email series (very open to feedback here, just respond to this email!). Each week for the next few, I’ll send out an email describing different parts of the output of GC.stat
. My hope is that by the end of it, we’ll understand how to read this output, and how it can help us learn what the garbage collector is doing!
Counts
To begin with, we’re going to look at the parts of this hash which describe how often Ruby garbage collection has run:
{ :count=>3025, :minor_gc_count=>2993, :major_gc_count=>32, :compact_count=>11, }
:major_gc_count
and :minor_gc_count
We may need a refresher on the difference between minor and major GC. They both exist because Ruby’s garbage collector uses generational garbage collection, predicated on the premise that most objects have short lives. Ruby looks only at younger objects on more frequent, minor GCs. (Notice how much bigger :minor_gc_count
is than :major_gc_count
in the above numbers.)
Once an object has survived three garbage collections, Ruby considers it old. While Ruby only looks at young objects in a minor GC, it looks at young and old objects in a major GC. If after a minor GC there still aren’t enough slots, Ruby will run a major GC.
We can also manually trigger a major GC by running GC.start(full_mark: true)
and a minor GC by running GC.start(full_mark: false)
.
Tip: Play around with these in an IRB console and watch GC.stat[:minor_gc_count]
and GC.stat[:major_gc_count]
increase! We can also create a bunch of objects that we know will be collected immediately (10_000.times { Object.new }
for example) and see how that affects GC counts!
:count
:count
in our hash above is the sum of minor and major GCs. GC.stat[:count] == GC.stat[:minor_gc_count] + GC.stat[:major_gc_count]
:compact_count
And then lastly, :compact_count
is the number of compactions that Ruby’s garbage collector has done. Compaction is the solution to fragmentation - running compaction reduces fragmentation in the Ruby heap. As a reminder, compaction is relatively new to Ruby and this feature is not enabled by default yet. To manually trigger a compaction, and watch GC.stat[:compact_count]
increase, we can run GC.compact
.
More next week!
As I mentioned earlier, I’ll be sending emails over the next few weeks to keep digging into different information that GC.stat
is giving us. I’m excited, and eager for feedback.
Sent with love from somewhere in the woods,
Jemma