Ruby Memory Pitfalls
Ruby has an automatic memory management. In most cases this is good; sometimes it becomes sad.
Ruby memory management is both elegant and cumbersome. It stores objects (named RVALUE
s)
in so-called heaps of size of approx 16KB. On a low level, RVALUE
is a c
-struct, containing
a union of different standard ruby object representations.
So, heaps store RVALUE
objects, which size is not more than 40 bytes. For such
objects as String
, Array
, Hash
etc. this means that small objects can fit in
the heap, but as soon as they reach a threshold, an extra memory outside of the
Ruby heaps will be allocated.
This extra memory is flexible; is will be freed as soon as an object became GC’ed. But the heaps themselves are not released to OS anymore.
Let’s take a look at the simple example:
def report
puts 'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`
.strip.split.map(&:to_i)[1].to_s + 'KB'
end
report
big_var = " " * 10_000_000
report
big_var = nil
report
ObjectSpace.garbage_collect
sleep 1
report
# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 11788KB
Here we allocate the huge amount of memory, use it somehow and then release back to OS. Everything seems to be fine. Let’s now slightly change the source code:
- big_var = " " * 10_000_000
+ big_var = 1_000_000.times.map(&:to_s)
That was a humdrum modification, wasn’t it? But wait:
# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 57448KB
WTF? The memory is not released to OS anymore. That’s because each element
of the array we introduced suits the RVALUE
size and is stored in the ruby heap.
In most cases this is OK. There are more empty slots in ruby heap now; code
re-run will not eat any additional memory; GC[:heap_used]
value is decreased
as expected every time we dispose big_var
and a lot of empty heaps, ready
for operation are returned back to Ruby. To Ruby that said, not to OS.
So, be careful with creating a lot of temporary variables suiting the 40 bytes:
big_var = " " * 10_000_000
big_var.gsub(/\s/) { |c| '-' }
results in growth of memory guzzled by Ruby as well. And this memory will not be returned back to OS during the whole long run:
# ⇒ Memory 10156KB
# ⇒ Memory 13788KB
# ⇒ Memory 13788KB
# ⇒ Memory 12808KB
Not so crucial, but noteworthy enough.