Garbage collection and memory
GC.stat
GC.stat is a snapshot of the collector: how many objects are alive, how many were allocated in total, how often the collector ran and how much time it used. One key answers most questions:
ruby -e 'puts GC.stat[:heap_live_slots]'
23020
A bare Ruby already holds a few thousand objects, so the number alone means little. Read it twice and look at the difference, which is the memory the work actually kept.
Live slots
The same reading before and after allocating a hundred thousand strings. The gap is the garbage collector's backlog, not just the array:
before = GC.stat[:heap_live_slots]
strings = Array.new(100_000) { "x" * 10 }
after = GC.stat[:heap_live_slots]
p before: before, after: after
ruby gc.rb
{before: 23035, after: 126849}
The difference is about a hundred thousand slots, which is the array plus one string per entry. A loop that builds the strings and throws them away leaves the live count flat but raises total_allocated_objects, and that is the number that predicts a collection pause.
ObjectSpace
ObjectSpace.count_objects breaks the heap down by type. A string-heavy program shows it at once:
p ObjectSpace.count_objects[:T_STRING]
p GC.stat[:total_allocated_objects]
ruby gc.rb
117953 235417
Nearly half of the allocations in this tiny script are strings. A real application that builds a message per request allocates a dozen strings it never shows anyone; the fix is usually to build the message only when the level is enabled.
The strings that dominate
Three patterns account for most avoidable string churn:
- Interpolation in a loop. "id=#{id}" in a loop over a million rows allocates a million strings. Build it once outside, or write to an IO.
- Concatenation with +. out += line copies the whole string each time and is quadratic. out << line appends, and the benchmark in the next lesson shows the difference.
- A frozen literal that is not frozen. A string literal in a method called in a loop allocates per call unless the file has the frozen_string_literal comment.
What to watch for
A high live count is not a leak. A leak is a count that climbs with every
request and never comes down; a high constant is a cache or a big
configuration. Compare two requests, not two minutes.
GC.start before a measurement forces a collection
and makes the number repeatable. It also hides the cost the production
process pays, so use it for a measurement and not in the code.
Next: Profiling a slow script
Article author: Arthur Isaev