Ruby 2.2: incremental GC and symbol GC
Ruby 2.2.0 arrives on Christmas Day with two garbage-collector improvements that matter for real apps — incremental GC to cut pause times, and symbol GC to finally close a long-standing memory leak and denial-of-service vector.
Right on schedule, Ruby 2.2.0 has arrived on Christmas Day. Where 2.1 gave us the generational collector, 2.2 continues the theme of making the runtime better-behaved for the long-running server applications most of us actually ship. The two headline changes are both about garbage collection, and both fix problems you may have hit without quite knowing what they were. Here is what is in the box.
Incremental GC: shorter pauses
Last year’s generational collector (RGenGC) reduced how much work the garbage collector does by scanning young objects far more often than old ones. But there was still a lurking problem: the occasional major GC — the one that examines the whole heap — was a “stop the world” event. Everything halts while it runs, and on a large heap that pause can be long enough to notice: a latency spike in a web request, a stutter in anything interactive.
Ruby 2.2 introduces incremental GC, which attacks the pause directly. Instead of doing the entire mark phase in one uninterruptible burst, it breaks that work into small increments interleaved with your program running. The total work is about the same, but it is spread out, so the worst-case single pause shrinks dramatically.
# you can observe the collector's behaviour:
GC.stat(:major_gc_count) # how many full collections have run
GC.stat(:minor_gc_count) # how many cheap young-only collections
For a web app this is the most directly felt improvement in the release. The metric that matters in production is not average latency but the tail — the p99, the occasional slow request that a long GC pause produces — and incremental GC pulls that tail in. You upgrade, your latency graph gets a little flatter at the top, and you wrote no code to make it happen. It is the same philosophy as RGenGC: keep correctness identical, make the runtime kinder to servers.
Symbol GC: closing a real memory leak
This is the fix that resolves an actual, exploitable problem, and it is worth
understanding why it mattered. In every Ruby before 2.2, symbols were never
garbage collected. Once you created a symbol, it lived for the entire life of the
process. For the symbols you write as literals in your source (:name, :status)
that is fine — there is a fixed number of them. The danger was dynamically created
symbols:
# every distinct string here becomes a permanent symbol — pre-2.2, this leaks
params.each { |key, _| do_something(key.to_sym) }
"user_#{id}".to_sym # a brand-new immortal symbol per id
Any code path that turned untrusted, variable input into symbols was a slow,
unbounded memory leak — and worse, a denial-of-service vector. An attacker who could
get your app to call .to_sym on attacker-controlled strings (a classic was
mass-assigning or parsing input into symbol keys) could push the process to consume
memory without limit until it fell over. This was a genuine, repeatedly-exploited
class of vulnerability, and the standard advice was a defensive reflex: never call
to_sym on user input; prefer string keys for anything external.
Ruby 2.2 makes dynamically-created symbols garbage collectable. A symbol that is no longer referenced can now be reclaimed like any other object, which closes the leak and defuses the DoS. The long-standing “symbols are forever” caution is finally obsolete for dynamic symbols, and a whole category of careful workarounds can be relaxed. (You should still be thoughtful about turning unbounded external input into symbols, but it is no longer a ticking memory bomb.)
The smaller additions
A few quieter improvements round out the release:
Kernel#itselfis a tiny, genuinely useful addition: it just returns the receiver. It reads awkwardly described and obviously right in use — grouping by an element’s own value becomesarray.group_by(&:itself)instead of a noisy{ |x| x }block.- Better heap management. The GC tuning around how the heap grows and is trimmed has been improved, so long-running processes manage their memory more gracefully — fewer cases of a process holding onto a high-water-mark of memory it no longer needs.
- Frozen string optimisation continues. The work toward a frozen-string-literal
future advances, reinforcing the habit of
.freeze-ing string constants in hot paths. - Housekeeping. Some long-deprecated methods are removed and standard-library bits are pruned, the usual tidying that keeps the language coherent.
Should you upgrade?
Yes, and the security angle makes it more than the usual “nice to have”. The incremental GC is a free latency improvement for any server app — exactly the kind of change you adopt by bumping a version number and watching your tail latency improve. But the symbol GC is the one to act on deliberately: it closes a real memory-leak-and-DoS hole that has shaped defensive Ruby coding for years. If you run anything that processes untrusted input — which is to say, any web app — 2.2 removes a sharp edge you have been carefully working around.
The pattern across 2.1 and now 2.2 is unmistakable and welcome: the core team is methodically hardening Ruby for production. Faster, generational, incremental collection; a closed DoS vector; gentler memory behaviour over long uptimes. None of it changes how you write Ruby day to day, and all of it makes the Ruby you have already written safer and smoother to run. A fittingly practical Christmas present.