Understand How Ruby Uses Memory

Ruby stores objects in its own heap, and uses operating system heap for data that doesn’t fit into objects. Let’s see how that works.

Objects

In Ruby everything is an object that’s internally represented as the RVALUE struct. sizeof(RVALUE) is machine dependent:

  • 20 bytes in 32-bit architecture when double is 4-byte aligned

  • 24 bytes in 32-bit architecture when double is 8-byte aligned

  • 40 bytes in 64-bit architecture

Most modern computers are 64-bit, so we’ll assume that one object costs us 40 bytes of memory to create.

You can see the object size on your computer in the debugger. I’m running my examples on 64-bit Linux, so I’ll use the gdb debugger. If you are on Mac OS X, try lldb that comes with the Xcode command-line tools. It’s compatible with gdb, and examples from this book should run without modification. On Windows, the easiest way is to install MinGW,[38] which includes the gdb debugger.

This is how you can get the object size in the debugger:

 
$ ​gdb `rbenv which ruby`
 
(gdb) p sizeof(RVALUE)
 
$1 = 40
 
(gdb)

As expected, it’s 40 bytes on my 64-bit Linux system.

Note that gdb requires the argument to be an executable and not a shell wrapper, for example, the one that rbenv installs. That’s why I call rbenv which to get the actual path to the Ruby executable and pass it to the debugger.

Is 40 bytes per object a large overhead? It doesn’t sound like a lot, but numbers will add up quickly at runtime.

A medium-sized Rails application will allocate half a million objects at startup. That translates to about 20 MB of memory just to store the objects. And this estimate doesn’t include extra space that you might need to store objects’ data.

In addition to that, to save time during object creation, Ruby preallocates extra space and thus uses even more memory than necessary to keep all existing objects. Let’s see how.

Ruby Objects Heap

Ruby allocates objects in the heap space that consists of heap pages. Each heap page in turn is divided into slots, one slot for one object.

When Ruby wants to allocate an object, it takes up the unused slot from the heap. If there are not enough free slots, Ruby grows the heap space by adding one or more heap pages. How much is added depends on the current heap usage, interpreter version, and the heap growth algorithm parameters.

Ruby 1.8

Ruby 1.8 adds one heap page at a time. The first page that is created at startup contains HEAP_MIN_SLOTS slots. This constant is defined in gc.c and is equal to 10000 by default.

Subsequent pages will be larger by a factor of 1.8. For example, the second page will have 18,000 slots; the third, 32,400; and so on. The total number of slots will be 10,000, 28,000, 60,400, and so on.

Ruby 1.9 and 2.0

Ruby 1.9 and later use a slightly different heap growth algorithm. First, the heap growth factor defines the growth of the whole heap space, not the individual heap page as in 1.8. Second, instead of adding one ever-increasing heap page at a time, it preallocates several fixed-sized heap pages.

Smaller heap pages theoretically help to reduce memory fragmentation and make it easier to reclaim unused heap space. We’ll explore later in this chapter how well that theory corresponds to real life.

Each heap page in Ruby >= 1.9 is 16 kB (minus extra several dozen bytes required for upkeep); 16 kB is enough to keep, for example, 408 objects on a 64-bit Linux system. The HEAP_OBJ_LIMIT constant tells us the number of objects in a heap page:

 
$ ​gdb `rbenv which ruby`
 
(gdb) p HEAP_OBJ_LIMIT*1
 
$1 = 408
 
(gdb)

Let’s investigate how object preallocation works. First, we’ll talk about Ruby 1.9, the simplest of modern implementations.

At startup, Ruby 1.9 preallocates HEAP_MIN_SLOTS / HEAP_OBJ_LIMIT heap pages. With default settings, it’s 10,000 / 408 = 24 heap pages on my system.

When the interpreter needs to add more heap space, it takes the number of pages currently used, multiplies that by a factor of 1.8, and allocates the missing heap pages.

For example, once those 24 heaps created at startup are taken, Ruby grows the heap space to 24 * 1.8 = 43, adding (43 - 24) = 19 new heaps. These 43 heaps will have space for 43 * HEAP_OBJ_LIMIT = 43 * 408 = 17,544 object slots. Next time the heap grows to 43 * 1.8 = 77 heaps, giving us 77 * 408 = 31,416 slots.

Note that with this algorithm, heap growth is much slower than in 1.8, thus reducing total memory usage in applications.

You can see the heap growth for yourself with GC#stat in irb.

 
$ ​rbenv shell 1.9.3-p551
 
$ ​irb
 
irb(main):001:0> require ​'pp'
 
=> true
 
irb(main):002:0> pp GC.stat; nil
 
{:count=>7,
 
:heap_used=>77,
 
:heap_length=>77,
 
:heap_increment=>0,
 
:heap_live_num=>14208,
 
:heap_free_num=>17146,
 
:heap_final_num=>104}
 
=> nil

Let me explain the numbers that GC#stat returns:

count

The number of times GC ran.

heap_used

The number of heap pages allocated.

The larger this number is, the more memory our Ruby process consumes, and the more work GC has to do.

To estimate the Ruby heap memory consumption, multiply this by HEAP_OBJ_LIMIT and sizeof(RVALUE).

In our example, the memory used for heap is 77 * 408 * 40 = 1,256,640 bytes, about 1.2 MB.

The name of this parameter is misleading. It doesn’t necessarily mean that all of these heap pages are used. They might be allocated, but empty. Also, this number isn’t cumulative. It can be decreased if Ruby shrinks the heap space.

heap_increment

In theory, this should be the number of heap pages that can be allocated before the interpreter needs to run the GC and grow the heap space again.

In practice, this number is always 0 in Ruby 1.9.

heap_length

The total number of heap pages, including heap_used and heap_increment.

heap_live_num

The current number of live objects in the heap.

This number includes objects that are still live but will be collected next time GC runs. So we can’t use it to estimate the number of free slots in the heap.

heap_free_num

The current number of free object slots in the heap after the last GC run.

This number is also misleading and can’t be used to estimate the current number of free slots. The only way to know how much free space you have on the heap is to call GC.start before looking at the heap_free_num.

heap_final_num

The number of objects that weren’t finalized during the last GC and that will be finalized later.

Now that we know what GC#stat’s return values mean, let’s get back to our irb session and try to make sense out of the numbers we see.

After startup, irb has 77 heap pages allocated. This means there were three growth iterations: one at interpreter startup (24 pages), and two more during irb initialization (giving us 43 and 77 pages in total).

We can predict that the next time the heap grows, it will increase to 77 * 1.8 = 138 pages.

To observe this in irb, we must know how many extra objects we need to allocate. So we’ll force GC, and allocate slightly more than heap_free_num.

 
irb(main):003:0> GC.start
 
=> nil
 
irb(main):004:0> pp GC.stat; nil
 
{:count=>8,
 
:heap_used=>77,
 
:heap_length=>77,
 
:heap_increment=>0,
 
:heap_live_num=>12635,
 
:heap_free_num=>18764,
 
:heap_final_num=>0}
 
=> nil
 
irb(main):005:0> x = Array.new(19000) { Object.new }; nil
 
=> nil
 
irb(main):006:0> pp GC.stat; nil
 
{:count=>9,
 
:heap_used=>138,
 
:heap_length=>138,
 
:heap_increment=>0,
 
:heap_live_num=>30215,
 
:heap_free_num=>26156,
 
:heap_final_num=>42}
 
=> nil

Everything looks right. We have slightly fewer than 19,000 free slots on the heap. And when we allocate 19,000 objects, we observe the heap growth from 77 to 138 pages as predicted.

Ruby 2.1

Ruby 2.1 renames the HEAP_MIN_SLOTS constant to GC_HEAP_INIT_SLOTS and adds heap growth control. Instead of hard-coding 1.8, it defines the GC_HEAP_GROWTH_FACTOR constant. That still defaults to 1.8, though.

In addition we can set a cap on the heap growth. If we set the GC_HEAP_GROWTH_MAX_SLOTS constant, Ruby will add no more than the specified number of slots any time it grows the heap. This way we can force the heap growth to become linear at a certain point. We’ll talk a bit later about how that can be useful.

While the growth factor is still the same 1.8 times, heap growth is more conservative in Ruby 2.1. To calculate the heap pages increment, the interpreter takes only the number of allocated, nonempty slots, and multiplies that by the growth factor.

If we try to observe the growth effect in Ruby 2.1, we see that the heap length numbers are a bit off. For example, this is what I see when I run irb in Ruby 2.1.5:

 
$ ​rbenv shell 2.1.5
 
$ ​irb
 
irb(main):001:0> GC.stat[:heap_length]
 
=> 81

Why is the heap length 81 instead of 77 as in the similar Ruby 1.9 example earlier? It’s because Ruby 2.1 creates one heap page at startup before allocating the initial 24 pages. This way we get 25 pages, which then grow to 25 * 1.8 = 45, and then to 45 * 1.8 = 81.

GC#stat in Ruby 2.1 returns more information:

 
$ ​rbenv shell 2.1.5
 
$ ​irb
 
irb(main):001:0> require ​'pp'
 
=> true
 
irb(main):002:0> pp GC.stat; nil
 
{:count=>7,
 
:heap_used=>81,
 
:heap_length=>81,
 
:heap_increment=>0,
 
:heap_live_slot=>32505,
 
:heap_free_slot=>501,
 
:heap_final_slot=>0,
 
:heap_swept_slot=>19441,
 
:heap_eden_page_length=>81,
 
:heap_tomb_page_length=>0,
 
:total_allocated_object=>93148,
 
:total_freed_object=>60643,
 
:malloc_increase=>322184,
 
:malloc_limit=>16777216,
 
:minor_gc_count=>5,
 
:major_gc_count=>2,
 
:remembered_shady_object=>195,
 
:remembered_shady_object_limit=>300,
 
:old_object=>10585,
 
:old_object_limit=>11680,
 
:oldmalloc_increase=>2470360,
 
:oldmalloc_limit=>16777216}
 
=> nil

Here are some of the parameters that are relevant to heap allocation:

count, heap_used, heap_length, heap_increment

Same as in Ruby 1.9, except that heap_increment is not always zero anymore.

The heap space in Ruby 2.1 and later grows gradually. When Ruby grows the heap space, it only allocates the space in the list of heap pages. heap_length tells you the length of that list. Actual pages are allocated on demand. heap_used shows how many of them are allocated at any given time. heap_increment shows how many pages can be allocated.

This change means that newer Ruby interpreters grow the heap space gradually, without the spikes in memory usage.

heap_live_slot, heap_free_slot, heap_final_slot

Same as heap_live_num, heap_free_num, and heap_final_num in Ruby 1.9.

One important difference is that heap_free_num refers to the number of free slots in the allocated heap pages. So to estimate the free space in the heap, add the number of slots that can be allocated in the heap, like this: heap_increment * HEAP_OBJ_LIMIT + heap_free_num.

heap_swept_slot

The number of slots swept (freed) during the last GC.

total_allocated_object, total_freed_object

The number of allocated and freed objects during the process lifetime.

heap_eden_page_length, heap_tomb_page_length

The number of heap pages in eden and tomb.

The heap space in Ruby 2.1 and later is divided into eden and tomb. This is the Ruby way to keep track of occupied and empty heap pages. The former are in the eden; the latter go into the tomb.

When allocating objects, Ruby looks for free space in the eden pages first. Only if there’s no space in eden, it takes a free page from tomb. This algorithm is good for two reasons. First, it reduces memory fragmentation by reusing empty pages only as necessary. But second, and most important, it gives more opportunity to the interpreter to destroy them and free up unused heap space.

Let me explain why.

Ruby interpreters before 2.1 could not really shrink the heap space. Once allocated, the page was likely to stay there until the program exits. For example, in Ruby 1.8 each subsequent heap page is bigger than the previous one by a factor of 1.8. And the bigger the page is, the less likely it is to become empty and be a candidate for destruction. Even fixed-size pages in Ruby 1.9 are not that likely to be freed because of memory fragmentation.

So, theoretically, Ruby 2.1 should have a better chance of freeing up the heap space. Let’s see if that works. First, we’ll start irb and create 100,000 objects:

 
$ ​rbenv shell 2.1.5
 
$ ​irb
 
irb(main):001:0> GC.start
 
=> nil
 
irb(main):002:0> ​"eden: %d, tomb: %d"​ % [GC.stat[:heap_eden_page_length],
 
GC.stat[:heap_tomb_page_length]]
 
=> ​"eden: 81, tomb: 0"
 
irb(main):003:0> x = Array.new(100000) { Object.new }; nil
 
=> nil
 
irb(main):004:0> ​"eden: %d, tomb: %d"​ % [GC.stat[:heap_eden_page_length],
 
GC.stat[:heap_tomb_page_length]]
 
=> ​"eden: 277, tomb: 0"

So far, all pages are on the eden because they contain live objects. Now let’s clear the reference to our array of objects and call GC to free it up:

 
irb(main):005:0> x = nil
 
=> nil
 
irb(main):006:0> GC.start
 
=> nil
 
irb(main):007:0> ​"eden: %d, tomb: %d"​ % [GC.stat[:heap_eden_page_length],
 
GC.stat[:heap_tomb_page_length]]
 
=> ​"eden: 84, tomb: 168"

Your numbers might be a bit different, but you should see the same pattern. From 277 eden pages, 84 remained used. That’s almost the same number of pages (81) as before we created our 100,000 objects.

A total of 168 pages became empty and got into the tomb. The rest, 277 -- 84 -- 168 = 25 pages were freed up. If we translate that into bytes, we’ll see that Ruby freed up 400 kB (25 pages) out of 4.3 MB (277 pages), reducing the heap size by about 9%. This reduction is not significant, but it indeed happens.

Why did Ruby decide to keep 168 unused pages in the heap? It’s because it doesn’t want to shrink heap space too much. The interpreter makes an assumption that if you created a lot of objects before, you’ll tend to continue creating a lot of objects in the future. That’s not always true, but I can’t say the assumption is unreasonable.

This is the algorithm that Ruby uses to determine the number of heap pages to free.

  1. Take the number of pages swept during GC, meaning pages where GC found at least one object to collect.

    In our case we dereferenced 100,000 objects, so GC had to sweep about 100,000 / HEAP_OBJ_LIMIT = 245 pages.

  2. Calculate the maximum number of slots that should stay in the heap. This number is either 80% of total heap slot count or GC_HEAP_INIT_SLOTS, whichever is bigger.

    In our case it’s 80% of 277 = 221 pages.

  3. The number of pages to be freed is the difference between these two numbers. If the difference is negative, then no pages will be freed.

    In our case the number of pages to free is 245 -- 221 = 24. In reality Ruby freed up 25 pages, mostly because GC had more pages to sweep due to fragmentation.

As you see, the heap space can indeed shrink in Ruby 2.1, but by not too much. Ten percent is about the maximum reduction you’ll ever see. This is certainly not enough to prevent your Ruby process from growing over time because, by default, growth is 80% while reduction is only 10%.

So in production just assume that your Ruby process will always grow, and be sure to periodically restart the long-running processes as we discussed back in Cycle Long-Running Instances.

Ruby 2.2

Ruby 2.2 changes GC#stat parameters yet again, and introduces some new ones. Let’s review them:

 
$ ​rbenv shell 2.2.0
 
$ ​irb
 
irb(main):001:0> require ​'pp'
 
=> true
 
irb(main):002:0> pp GC.stat
 
{:count=>7,
 
:heap_allocated_pages=>74,
 
:heap_sorted_length=>75,
 
:heap_allocatable_pages=>0,
 
:heap_available_slots=>30160,
 
:heap_live_slots=>29620,
 
:heap_free_slots=>540,
 
:heap_final_slots=>0,
 
:heap_marked_slots=>11631,
 
:heap_swept_slots=>10126,
 
:heap_eden_pages=>74,
 
:heap_tomb_pages=>0,
 
:total_allocated_pages=>74,
 
:total_freed_pages=>0,
 
:total_allocated_objects=>90529,
 
:total_freed_objects=>60909,
 
:malloc_increase_bytes=>212728,
 
:malloc_increase_bytes_limit=>16777216,
 
:minor_gc_count=>5,
 
:major_gc_count=>2,
 
:remembered_wb_unprotected_objects=>180,
 
:remembered_wb_unprotected_objects_limit=>278,
 
:old_objects=>10922,
 
:old_objects_limit=>11004,
 
:oldmalloc_increase_bytes=>1503376,
 
:oldmalloc_increase_bytes_limit=>16777216}

Let’s again ignore malloc parameters, and look only at those relevant to heap space:

heap_allocated_pages, heap_allocatable_pages, heap_sorted_pages

These are the heap_used, heap_increment, and heap_length variables that we know from Ruby 2.1.

heap_live_slots, heap_free_slots, heap_final_slots

Same as heap_live_num, heap_free_num, heap_final_num in Ruby 2.1.

heap_available_slots

The number of all available slots: heap_live_slots + heap_free_slots + heap_final_slots.

heap_marked_slots, heap_swept_slots

The number of slots marked and swept during the last GC.

heap_eden_pages, heap_tomb_pages

Same as heap_eden_page_length and heap_tomb_page_length.

total_allocated_pages, total_freed_pages, total_allocated_objects, total_freed_objects

In addition to the total cumulative numbers of allocated and freed objects, Ruby 2.2 lets us know the total number of allocated and freed pages during the interpreter’s lifetime.

Ruby 2.2 takes an even more conservative approach to growing the heap space than 2.1. Before, the growth was relative to the number of allocated pages (heap_used or heap_allocated_pages. Starting from 2.2, the growth is relative to the number of eden pages (heap_eden_pages). That number may be smaller, because allocated pages may go into the tomb if unused.

You can see this for yourself in irb, similarly to how we did it earlier for Ruby 1.9 and 2.1. Don’t worry if the page and slot numbers are not exactly what you think they should be. Ruby can reuse object slots and free or resurrect heaps. That complicates calculations. Just make sure you understand the big picture.

Now you know how memory is allocated and used for Ruby objects. By looking at GC#stat parameters you can find out how much memory is needed, how much can be allocated without doing GC, and what will happen to the object space after GC. This is necessary to know when you are stuck with high memory usage, and want to understand what happens.

But that’s not the whole story about memory usage. Remember, a Ruby object is 40 bytes at maximum. What if the data you’re storing into memory is larger than 40 bytes—for example, a large string that you’ve read from a file?

In Ruby that extra data doesn’t belong in the heap space, and is allocated and managed separately. Let’s see where and how.

Object Memory

A Ruby object can store only a limited amount of data, up to 40 bytes in a 64-bit system. Slightly less than half of that is required for upkeep. All data that does not fit into the object itself is dynamically allocated outside of the Ruby heap. When the object is swept by GC, the memory is freed.

For example, a Ruby string stores only 23 bytes in the RSTRING object on a 64-bit system. When the string length becomes larger than 23 bytes, Ruby allocates additional memory for it. We can see how much by calling ObjectSpace#memsize_of, for example, like this:

 
$ ​rbenv shell 2.2.0
 
$ ​irb
 
irb(main):001:0> require ​'objspace'
 
=> true
 
irb(main):002:0> str = ​'x'
 
=> ​"x"
 
irb(main):003:0> ObjectSpace.memsize_of(str)
 
=> 40
 
irb(main):004:0> str = ​'x'​*23
 
=> ​"xxxxxxxxxxxxxxxxxxxxxxx"
 
irb(main):005:0> ObjectSpace.memsize_of(str)
 
=> 40
 
irb(main):006:0> str = ​'x'​*24
 
=> ​"xxxxxxxxxxxxxxxxxxxxxxxx"
 
irb(main):007:0> ObjectSpace.memsize_of(str)
 
=> 65

In this example we first create a string with one character. It fits into the Ruby object, so ObjectSpace#memsize_of reports that 40 bytes is used. If we run the same example in Ruby 2.1, we’ll see 0 in the output. That’s because older interpreters do not include the size of the Ruby object itself into the number returned by ObjectSpace#memsize_of.

When the string is more than 23 bytes, it’s stored outside the object. The total size of the large string is 65 bytes: 40 for the Ruby object on the heap, 24 bytes dynamically allocated outside of the heap to store the whole string, and 1 byte for upkeep.

Files, long arrays, large hashes, and others get extra memory in the same way.

Unlike the heap space, Ruby allocates this additional object memory only on demand, and frees it up when the object is finalized.

So, if our program creates a large string and then discards its contents, the memory goes back to the operating system right away. Let’s see for ourselves:

chp10/additional_object_memory.rb
 
puts ​"memory usage at start %d MB"​ %
 
(`ps -o rss= -p #{Process.pid}`.to_i/1024)
 
 
str = ​"x"​ * 1024 * 1024 * 10 ​# 10 MB
 
 
puts ​"memory usage after large string creation %d MB"​ %
 
(`ps -o rss= -p #{Process.pid}`.to_i/1024)
 
 
str = nil
 
GC.start(full_mark: true, immediate_sweep: true)
 
 
puts ​"memory usage after string is finalized %d MB"​ %
 
(`ps -o rss= -p #{Process.pid}`.to_i/1024)
 
$ ​ruby additional_object_memory.rb
 
memory usage at start 8 MB
 
memory usage after large string creation 19 MB
 
memory usage after string is finalized 8 MB

Because this memory is allocated outside the heap space, it has almost no effect on garbage collection time. So it may be actually OK to allocate large amounts of data in memory if it ends up in the extra memory of one object.

For example, after reading the large file we’ll get one big string object in memory. When we’re done with the file contents, the memory it used goes back to the operating system. But if we parse the file, we might create a large number of Ruby objects, increasing the heap size and total memory usage more or less permanently.

That said, the very fact that we allocate extra memory might trigger GC. So yes, by allocating one big object we won’t be adding work for GC, but we might be increasing the number of collections required. There’s no free lunch in Ruby.

Now that you how Ruby allocates memory, you can understand what exactly GC is doing for us. Ruby can run without any GC, but it’d take too much memory. So the only task GC has to do is keep the memory usage of the Ruby process within reasonable limits. Let’s see how that works.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.106.233