From 12f9b9fe57f6af08937ffb4205c9c92a0bae4ea4 Mon Sep 17 00:00:00 2001 From: Wilfried OLLIVIER Date: Sun, 20 Apr 2025 20:51:11 +0200 Subject: [PATCH] content: Add demystifying Ruby 3/3 --- content/post/09-ruby-memory.md | 270 +++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 content/post/09-ruby-memory.md diff --git a/content/post/09-ruby-memory.md b/content/post/09-ruby-memory.md new file mode 100644 index 0000000..aa4f60d --- /dev/null +++ b/content/post/09-ruby-memory.md @@ -0,0 +1,270 @@ +--- +title: "Demystifying Ruby ♦️ (3/3): Memory Shenanigans" +subtitle: "How does Ruby do memory management ?" +date: 2025-04-23 +draft: false +author: Wilfried +tags: [dev, languages, ruby, demystifying-ruby] +--- + +Garbage collection ([GC](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science))) is an essential process +in any programming language that manages memory for the programmer. In Ruby (this article is about +the [MRI](https://en.wikipedia.org/wiki/Ruby_MRI)), the garbage collector plays a crucial role in ensuring efficient +memory usage and preventing memory leaks. + +## The Heap and the Stack + +Memory in a Ruby program is divided into two main regions: + +- **The Heap**: This is where dynamically allocated memory lives. Objects created at runtime, such as strings, arrays, + and hashes, are stored here. The garbage collector primarily operates on the heap to free up unused memory. +- **The Stack**: This is where method calls, local variables, and control flow data are stored. The stack is managed + automatically and follows a Last In, First Out (LIFO) structure. Each time a method is called, a new stack frame is + created; when the method completes, its stack frame is removed. + +Ruby’s garbage collector is responsible for: + +1. **Memory Allocation**: When a new object is created, Ruby allocates memory from the heap to store it. +2. **Garbage Collection**: When an object is no longer referenced, it becomes eligible for garbage collection. The GC + process reclaims memory from unused objects and makes it available for future allocations. + +## Two side of the same coin: The Tail (AKA Collection) + +Let’s dive in first into Garbage Collection. When thinking about objects, we can represent them as a tree of sub-objects +linked to a particular node known as the Root Set. Let’s consider this example. + +```ruby +data = { + person: { + name: "John", + age: 30, + address: { + city: "Paris", + country: "France" + } + } +} +``` + +In this code example, `data` is stored on the stack while containing a pointer (reference) to a Hash object on the heap. + +The hash can be described using this tree: + +[![](https://mermaid.ink/img/pako:eNptkM1OwzAQhF_F2kuLlFZtSdrGByT6h-CEKCcwB6tekghiRxtHokR5d9apQFDhi70z3-7YbuHgDIKEjHSVi8eNskJcPyt4cM6LPXoFL2I0uhIr1iqk2lnRio7VAAqx6s01m1aXKMXgzuV2IIZ7T4XNLs64DXM6Y-xyIoa31mOGdM5sA2MMYV3_Sdr27o7dQ-GPnHSvqaj_iTqBNwF0jfUU2B1pe8AzWFmI-N2FAempwQhKpFKHEtowSoHPsUQFko9G05sCZTvuqbR9cq78biPXZDnIV_1ec9VURnvcFJp_tPxRCa1BWocLgZwl_QyQLXyAjNPZeL6cJpM4The8pxEcQS7n45RXkkxZmieLpIvgsw-djJeLOP29ui_5K4UX?type=png)](https://mermaid.live/edit#pako:eNptkM1OwzAQhF_F2kuLlFZtSdrGByT6h-CEKCcwB6tekghiRxtHokR5d9apQFDhi70z3-7YbuHgDIKEjHSVi8eNskJcPyt4cM6LPXoFL2I0uhIr1iqk2lnRio7VAAqx6s01m1aXKMXgzuV2IIZ7T4XNLs64DXM6Y-xyIoa31mOGdM5sA2MMYV3_Sdr27o7dQ-GPnHSvqaj_iTqBNwF0jfUU2B1pe8AzWFmI-N2FAempwQhKpFKHEtowSoHPsUQFko9G05sCZTvuqbR9cq78biPXZDnIV_1ec9VURnvcFJp_tPxRCa1BWocLgZwl_QyQLXyAjNPZeL6cJpM4The8pxEcQS7n45RXkkxZmieLpIvgsw-djJeLOP29ui_5K4UX) + +Since memory is limited, the goal of garbage collection is to scan memory for unused data chunks that can be freed and +reused by the program. + +Using this example, it our Ruby VM execute a line like this one + +```ruby +person[:address] = nil +``` + +This will be represented as + +[![](https://mermaid.ink/img/pako:eNptkctuwjAQRX_FGhZQySAeCRAjVSpPtauqdNWmCyseEovEjmxHhSL-vQ6IllbMwvbce2ZGtg-QaIHAIDW8zMjrPFbEx8N7DC9aO7JGF8MHabfvydRrJRqrFTmQo1fP6PRkzrypeIGMNJ90ppqktXZGqvTuHzf3HE89NuiS1qNymKL5ZRa1K4RBa__MWJxql95NpNv7Gc_cSHtjyBlc1aCulDM1uzRcJXgDPq9Jzq2d44YomZONzHPWED0cCUGJdUZvkTW6m7A36F_y9qcULmP9ckdJonNtLsDkqiFZ0CVd1T0nQP3jSgHMmQopFGgKXqdwqPkYXIYFxsD8UXCzjSFWR19TcvWmdXEpM7pKM2AbnlufVaXgDueS-28rflSDSqCZ1TcHFg5OPYAdYAcsiPqd4bgXdoMgGvk9orAHNh52Ih9h2PPSMByFRwpfp6HdzngURNdx_AYfa6O2?type=png)](https://mermaid.live/edit#pako:eNptkctuwjAQRX_FGhZQySAeCRAjVSpPtauqdNWmCyseEovEjmxHhSL-vQ6IllbMwvbce2ZGtg-QaIHAIDW8zMjrPFbEx8N7DC9aO7JGF8MHabfvydRrJRqrFTmQo1fP6PRkzrypeIGMNJ90ppqktXZGqvTuHzf3HE89NuiS1qNymKL5ZRa1K4RBa__MWJxql95NpNv7Gc_cSHtjyBlc1aCulDM1uzRcJXgDPq9Jzq2d44YomZONzHPWED0cCUGJdUZvkTW6m7A36F_y9qcULmP9ckdJonNtLsDkqiFZ0CVd1T0nQP3jSgHMmQopFGgKXqdwqPkYXIYFxsD8UXCzjSFWR19TcvWmdXEpM7pKM2AbnlufVaXgDueS-28rflSDSqCZ1TcHFg5OPYAdYAcsiPqd4bgXdoMgGvk9orAHNh52Ih9h2PPSMByFRwpfp6HdzngURNdx_AYfa6O2) + +This is where the garbage collector comes in. The GC will look for memory chunks not linked to the RootSet (blocks +highlighted in green) to mark those and collect them in order to free memory space. So that the memory looks like this, +after collection : + +[![](https://mermaid.ink/img/pako:eNpdj0FPwzAMhf-K5cuG1FUtNF2bAxJbL3BknCAcImLSCppUWSoBVf87XieENF-c9_y9xJnwzRtCiTbooYWnRjnguntR-Oh9hANFha-w2dzCjr2BwtE7mGBm94zuluGeh073JGH14Fu3gvUhhs7ZqwuuYU5bxm4yWN-7SJbCmcGEd-gMyhhGSrCn0OuTxOmUVxhb6kmh5KPR4UOhcjNnBu2eve__YsGPtkX5rj-PrMbB6EhNp_l3_wg5Q2HvRxdRlvlyBcoJv1AW9XVaVrnIiqLecq8T_EZZlWnNJUTOVim2Yk7wZ3kzSytWv36JX-U?type=png)](https://mermaid.live/edit#pako:eNpdj0FPwzAMhf-K5cuG1FUtNF2bAxJbL3BknCAcImLSCppUWSoBVf87XieENF-c9_y9xJnwzRtCiTbooYWnRjnguntR-Oh9hANFha-w2dzCjr2BwtE7mGBm94zuluGeh073JGH14Fu3gvUhhs7ZqwuuYU5bxm4yWN-7SJbCmcGEd-gMyhhGSrCn0OuTxOmUVxhb6kmh5KPR4UOhcjNnBu2eve__YsGPtkX5rj-PrMbB6EhNp_l3_wg5Q2HvRxdRlvlyBcoJv1AW9XVaVrnIiqLecq8T_EZZlWnNJUTOVim2Yk7wZ3kzSytWv36JX-U) + +There are plenty of strategies to get this done. The most basic one is *mark and sweep.* As the name implies, it first: + +- **marks: start from the root and follow edges, marking all reachable objects** + +then it + +- **sweeps: all objets that are not marked are freed** + +Rince and repeat, and voila, you have a garbage collector. + +**A Garbage Collector is a responsible for finding no longer used object and free the memory attached to it** + +One issue with this approach is that we need a static view of the memory space—even if only briefly—because we can't +perform garbage collection while allocating memory. + +his requires a "stop the world" pause while performing GC tasks, the program cannot execute other logic. Stopping the +entire program and scanning the complete object tree is slow and resource-intensive, suggesting we need a better +approach. + +To address the limitations of basic garbage collection methods like "mark and sweep," Ruby employs a more advanced +technique called **generational garbage collection (GC)**. This approach is based on the observation that most objects +in a program are short-lived, meaning they are created and discarded quickly. + +By dividing objects into different "generations" based on their lifespan, generational GC optimizes memory management by +focusing its efforts on newly created objects, which are more likely to be garbage, while less frequently scanning older +objects that are more likely to persist. + +This will reduce the number of node to visit since the older objects will not be visited as often as the object in the +yonger ones. + +**A Generational Garbage Collection ensure that we focus GC efforts on short lived objects, scanning the stable less often +to ensure a good balance between efficiency and performance** + +[![](https://mermaid.ink/img/pako:eNptUE1PwzAM_SuRLxvSmFpYuzUHpG3dJjghxgnCIaymraBJlaYSo-p_xylbx1cufn5-9nPcwE4nCBxSI8uM3cdCMTZ_FHCntWVbtAKeHFXVz1-KDSqfyhSYf6gxtiCmRFNpxRrW9vSSaCUL5GxwozM1YMOtNblKz3pFTAqZkuDSY8NrZTFFc6yiSoT67e0dvL1-xMqNSBKDVfXDfE38Lrd7Mr-VJq_-cd84ia6VNU61NlLt8I-sX2POzs-v2MLBRQeXJxif4MrBVQfXJ7iBEZ04T4BbU-MICjSFdCk0TiTAZligAE4wkeZVgFAt9ZRSPWhdHNuMrtMM-It8qyiry0RajHNJ5yl61tDGaJbuX8DDqJsBvIF34JPoYhzO_MCbTKIpRSrugc_CcUQvCHyiwmAatCP46Ey98WwaeN-e334CKr6kbg?type=png)](https://mermaid.live/edit#pako:eNptUE1PwzAM_SuRLxvSmFpYuzUHpG3dJjghxgnCIaymraBJlaYSo-p_xylbx1cufn5-9nPcwE4nCBxSI8uM3cdCMTZ_FHCntWVbtAKeHFXVz1-KDSqfyhSYf6gxtiCmRFNpxRrW9vSSaCUL5GxwozM1YMOtNblKz3pFTAqZkuDSY8NrZTFFc6yiSoT67e0dvL1-xMqNSBKDVfXDfE38Lrd7Mr-VJq_-cd84ia6VNU61NlLt8I-sX2POzs-v2MLBRQeXJxif4MrBVQfXJ7iBEZ04T4BbU-MICjSFdCk0TiTAZligAE4wkeZVgFAt9ZRSPWhdHNuMrtMM-It8qyiry0RajHNJ5yl61tDGaJbuX8DDqJsBvIF34JPoYhzO_MCbTKIpRSrugc_CcUQvCHyiwmAatCP46Ey98WwaeN-e334CKr6kbg) + +But there’s a hole in this—what if **Gen 1** is pointing to something in **Gen 0,** like in this example. If we strictly +scan Gen 0 memory space we don’t have the full picture to know that it’s actually referenced from a Gen 1 object and we +for sure, don’t want to free it! + +For this to work Ruby uses a **Remembered Set**, to track specific references from Gen 1 to Gen 0, meaning than when we +check Gen 0, we also check the Remembered Set, in order, as the name suggest, not forget something, avoiding freeing a +still in used object. Additionally, Ruby uses what we call a **write barrier** to track modifications during garbage +collection. The write barrier monitors memory writes and updates the Remembered Set if needed. + +**A Rembered Set ensures that when scanning young generation memory space, we still have some hints about the existing +link with older generation, to ensure we do not free an object referenced from the older generation** + +Is this sufficient? Partially. As mentioned earlier in this article, scanning the memory space requires a * +*Stop-the-World** pause for the entire duration of the process to ensure proper memory cleanup. Rails applications, by +nature, tend to consume a significant amount of memory. The larger the memory footprint, the longer the pause for +garbage collection. + +But what if we could make garbage collection **interruptible** ? If we could pause and resume the GC at will, we’d have +much better control over execution time, ensuring we don’t spend excessive time collecting garbage when an HTTP request +needs processing. This is where **tri-color marking GC** comes in, allowing incremental garbage collection while +minimizing application pauses. + +[![](https://mermaid.ink/img/pako:eNp9U9uO2jAQ_ZWRV_sWVgTCzUiVulDRSlt11W1VqaUPJhmSlCSOHKeQAv_esU0oy7adB2zjc5kzhj0LZYSMs1iJMoFP8-myAKonLZT-tmR2hYVQKxEjzGSWYahTWSzZd-h0XsF7oTYEMws8JqJCDu8iLHS6bkChCBOxyhDk6gfRKiI5dQsn-uFLkmqEz8UZeoCnLWJpnM3aan7EMBNpDjnmUjWkwznfGu6V3kKJBh6VDLGq0iI-tHvSO-3AQj6cGyIhit44nRZjpO4zEW7gIf2JLfpgfYiwMlcvGXNZ4DNzG-E0T5vGTOxNEVE39AlyDYsZzJowQzsZh6zqlXuLB4yxiNyXpl4Tzc6LXw7sxSxM3RPU5OQX7VxnNTUjnM3JbVAHuUhn_d329hbepnHSCWWhlag0VLrJsHKX9DhVNcc12D5gnWYZv1nb8iqt5Ab5TdfW6djZppFOeK_ceaHMpGqvp86prBWetFaxB7Yj0LjTV3YmzcltGI4Go-js1g_6Ivi3m-ttegoWmZ-P1TJuzvcvbq4NZ_cszVXW_9mRm5O5dmIe_QXTiHGtavRYjioX5sj2posl0wnm9ECctqbdJVsWR-KUovgqZd7SlKzjhPG1yCo61WUkNM5TQdn-QOhRUc1kXWjGB76VYHzPdoxP_Lvh2A8mgT-eDHvBwGMN474_vJtQdYN-tz_qjke9o8d-Wc_u3Xg0OP4G35VgPA?type=png)](https://mermaid.live/edit#pako:eNp9U9uO2jAQ_ZWRV_sWVgTCzUiVulDRSlt11W1VqaUPJhmSlCSOHKeQAv_esU0oy7adB2zjc5kzhj0LZYSMs1iJMoFP8-myAKonLZT-tmR2hYVQKxEjzGSWYahTWSzZd-h0XsF7oTYEMws8JqJCDu8iLHS6bkChCBOxyhDk6gfRKiI5dQsn-uFLkmqEz8UZeoCnLWJpnM3aan7EMBNpDjnmUjWkwznfGu6V3kKJBh6VDLGq0iI-tHvSO-3AQj6cGyIhit44nRZjpO4zEW7gIf2JLfpgfYiwMlcvGXNZ4DNzG-E0T5vGTOxNEVE39AlyDYsZzJowQzsZh6zqlXuLB4yxiNyXpl4Tzc6LXw7sxSxM3RPU5OQX7VxnNTUjnM3JbVAHuUhn_d329hbepnHSCWWhlag0VLrJsHKX9DhVNcc12D5gnWYZv1nb8iqt5Ab5TdfW6djZppFOeK_ceaHMpGqvp86prBWetFaxB7Yj0LjTV3YmzcltGI4Go-js1g_6Ivi3m-ttegoWmZ-P1TJuzvcvbq4NZ_cszVXW_9mRm5O5dmIe_QXTiHGtavRYjioX5sj2posl0wnm9ECctqbdJVsWR-KUovgqZd7SlKzjhPG1yCo61WUkNM5TQdn-QOhRUc1kXWjGB76VYHzPdoxP_Lvh2A8mgT-eDHvBwGMN474_vJtQdYN-tz_qjke9o8d-Wc_u3Xg0OP4G35VgPA) + + +and **black (live, retained in memory)**. This approach enables **incremental garbage collection**, making it +**interruptible**, so the process can pause and resume as needed, reducing long application pauses. Of couse, as in the +previous example, there is some new corner cases we need to handle : + +1. what if a new object is created while GC is processing but paused: mark them directly as black object (using a write + barrier as in the remembered set) +2. what is we dereference objects alraedy processed: keep them as is, they will be marked white on the next GC loop! + +he evolution of Ruby's garbage collection followed a long journey: from **Stop-the-World GC**, which paused the entire +program, to **Generational GC (Ruby 2.1)**, which optimized collection by focusing on young objects, and finally to +**Tri-Color Marking GC (Ruby 2.2)**, enabling incremental, interruptible garbage collection for reduced pauses and better +responsiveness + +## Two side of the same coin: The Head (AKA Allocation) + +Let’s take a look at how **Ruby allocates memory** for objects under the hood. In CRuby (MRI), Ruby doesn't use the +system's `malloc` for every object. Instead, it manages its own heap, which is divided into chunks called **pages**. +Each page contains multiple **slots**, and each slot is capable of storing **one Ruby object**. + +MRI keeps track of which slots are available using a **free list**. Rather than using a separate data structure, Ruby +cleverly reuses the memory of free slots themselves: each free slot stores a pointer to the **next available slot**. +This forms a linked list of free slots, making allocation fast and efficient (no need to scan the entire heap !). + +[![](https://mermaid.ink/img/pako:eNqFUs1Kw0AQfpVlTgppbdps24QSUIt4qD-ovWhENs20jTZJ2WzAWnoTr4JexeJR8AF8nr6APoK7Sa2xBZ3L7s73twwzhk7kIVjQ42zYJydNJySy4sTNGg4cJe6I7GEQ8RE5ZD10IKOo2kXmIT9z4HP6eJui81bD5XbDt9cCFMxjgmmkyxEHfixIXxLI7O6BHA8iQcrrjQ3fduDcCX9sFaKnrtPXjKYrw3aMHimQA_cSO4JspqK8pCwls7enubFS7MhQdYZ4LS7UD8js-eXj_T6jGCsOlV-hldXQrRWJkQ81_g-lKw4070D_dNhvt1pLs8rmTQoFO5vB0kgWgLH07QVAMwBDT9mCJjfB98ASPEENAuQBU08YK5oDoo-BXAFLXj3Gr9QyTKRmyMLTKAq-ZTxKen2wumwQy1cylCuATZ_JlQoWXS4TkW9HSSjAqtPUA6wxXINlmOVita7TkmGYNXmaGowkp1o0ZVGqy1aV1uhEg5s0tFSs1wwzX5MvmU3qFw?type=png)](https://mermaid.live/edit#pako:eNqFUs1Kw0AQfpVlTgppbdps24QSUIt4qD-ovWhENs20jTZJ2WzAWnoTr4JexeJR8AF8nr6APoK7Sa2xBZ3L7s73twwzhk7kIVjQ42zYJydNJySy4sTNGg4cJe6I7GEQ8RE5ZD10IKOo2kXmIT9z4HP6eJui81bD5XbDt9cCFMxjgmmkyxEHfixIXxLI7O6BHA8iQcrrjQ3fduDcCX9sFaKnrtPXjKYrw3aMHimQA_cSO4JspqK8pCwls7enubFS7MhQdYZ4LS7UD8js-eXj_T6jGCsOlV-hldXQrRWJkQ81_g-lKw4070D_dNhvt1pLs8rmTQoFO5vB0kgWgLH07QVAMwBDT9mCJjfB98ASPEENAuQBU08YK5oDoo-BXAFLXj3Gr9QyTKRmyMLTKAq-ZTxKen2wumwQy1cylCuATZ_JlQoWXS4TkW9HSSjAqtPUA6wxXINlmOVita7TkmGYNXmaGowkp1o0ZVGqy1aV1uhEg5s0tFSs1wwzX5MvmU3qFw) + +Once a page is full, Ruby allocates a new page for new objects to be stored. + +A slot is about 40-bytes, and a page is about 16kb (meaning 400+ objects per page). Knowing this, we now have a new +question, what if we want to store a gigantic string + +```ruby +test = "hi" * 1_000_000 +# 2 bytes * 1_000_000 = 2_000_000 bytes, so way more than 40! +``` + +For large objects like in this example (and other such as objects with many instance variables, big arrays, big hashes), +Ruby will **allocate the string’s data separately** on the **system** heap. It **does not store the full content of the +string in the slot**, but instead, it stores a **pointer** to the memory region where the string data is actually +stored. + +Now let’s bridge the gap between allocation and garbage collection. From the first paragraph, we know that there is a +mechanism in place to identify data that needs to be freed, but what does this mean at a pages and slots level? + +When Ruby’s GC runs, it doesn’t reclaim entire pages immediately. Instead, it works at the **slot level**. During the +sweep phase, every slot that holds an unreachable object is marked as **free** and linked back into the **free list**. +This means the slot is now ready for reuse in future allocations — super efficient, and no expensive system calls +needed. + +But here’s the catch: even if most of the slots in a page are free, Ruby **can’t release the page itself to the system** +unless **all** the slots are free. That means pages with just a handful of live objects become **partially unusable +while still being reserved for Ruby** and not released to the operating system for other programs to use. + +To deal with this memory fragmentation issue, Ruby introduced **compacting GC** in 2.7+. Compaction relocates live +objects from scattered pages into more densely packed ones. Once a page has **no live objects left**, it can be +completely **reclaimed**, either for reuse or even returned to the system and you have to call it manually + +```ruby +GC.compact +``` + +With all this theory in mind, we know have want we need to experiment with the Ruby VM ! + +## Fun with GC + +After theory, comes practice, let’s look at some GC related code and how we can explore this Ruby code. `objspace` will +be used as it’s the standard way in Ruby MRI to get details about the GC state and trigger GC related routines. + +```ruby +require 'objspace' + +GC.start +puts GC.stat + +10.times do + "a" * 100_000 +end + +GC.start +puts GC.stat + +{:count=>11, :time=>4, :marking_time=>3, :sweeping_time=>0, :heap_allocated_pages=>24, :heap_sorted_length=>201, :heap_allocatable_pages=>177, :heap_available_slots=>24453, :heap_live_slots=>18007, :heap_free_slots=>6446, :heap_final_slots=>0, :heap_marked_slots=>17974, :heap_eden_pages=>24, :heap_tomb_pages=>0, :total_allocated_pages=>24, :total_freed_pages=>0, :total_allocated_objects=>62153, :total_freed_objects=>44146, :malloc_increase_bytes=>880, :malloc_increase_bytes_limit=>16777216, :minor_gc_count=>7, :major_gc_count=>4, :compact_count=>0, :read_barrier_faults=>0, :total_moved_objects=>0, :remembered_wb_unprotected_objects=>0, :remembered_wb_unprotected_objects_limit=>175, :old_objects=>17508, :old_objects_limit=>35016, :oldmalloc_increase_bytes=>880, :oldmalloc_increase_bytes_limit=>16777216} +{:count=>12, :time=>4, :marking_time=>3, :sweeping_time=>1, :heap_allocated_pages=>24, :heap_sorted_length=>201, :heap_allocatable_pages=>177, :heap_available_slots=>24453, :heap_live_slots=>18022, :heap_free_slots=>6431, :heap_final_slots=>0, :heap_marked_slots=>18020, :heap_eden_pages=>24, :heap_tomb_pages=>0, :total_allocated_pages=>24, :total_freed_pages=>0, :total_allocated_objects=>62252, :total_freed_objects=>44230, :malloc_increase_bytes=>832, :malloc_increase_bytes_limit=>16777216, :minor_gc_count=>7, :major_gc_count=>5, :compact_count=>0, :read_barrier_faults=>0, :total_moved_objects=>0, :remembered_wb_unprotected_objects=>0, :remembered_wb_unprotected_objects_limit=>179, :old_objects=>17952, :old_objects_limit=>35904, :oldmalloc_increase_bytes=>832, :oldmalloc_increase_bytes_limit=>16777216} + +``` + +- One major GC occurred after the string allocations (`:major_gc_count` +1). +- Only 15 new live objects remained after allocations — Ruby GC cleaned up fast. +- Heap size stayed stable (no new pages allocated). +- GC was fast — total time stayed at 4ms, with minimal sweeping. +- No compaction was triggered (:compact_count still 0). + +```ruby +require 'objspace' + +objs = [] + +10_000.times do + objs << "x" * rand(100..1000) +end + +# Remove some random ones to fragment the heap +5000.times { objs.delete_at(rand(objs.size)) } + +GC.start +puts "Before compaction:" +puts GC.stat.slice(:heap_eden_pages, :heap_live_slots, :heap_free_slots, :heap_allocated_pages, :compact_count) + +GC.compact + +puts "\nAfter compaction:" +puts GC.stat.slice(:heap_eden_pages, :heap_live_slots, :heap_free_slots, :heap_allocated_pages, :compact_count) + +Before compaction: +{:heap_eden_pages=>145, :heap_live_slots=>48028, :heap_free_slots=>76266, :heap_allocated_pages=>145, :compact_count=>0} + +After compaction: +{:heap_eden_pages=>136, :heap_live_slots=>48054, :heap_free_slots=>68875, :heap_allocated_pages=>136, :compact_count=>1} + +``` + +| Value | Before | After | Commentary | +| --- | --- | --- | --- | +| `:heap_allocated_pages` | 145 | **136** | ✅ **9 pages were freed** — compaction successfully reduced memory usage! | +| `:heap_live_slots` | 48028 | 48054 | +26 | +| `:heap_free_slots` | 76266 | **68875** | 🔻 Freed slots dropped because Ruby **repacked objects more densely**, using fewer pages | +| `:compact_count` | 0 | 1 | Ensuring we have desired state | + +Ruby’s garbage collector handle memory management for you, quietly keeping things running smoothly behind the scenes. +It’s got all the right tools in its belt: generational collection for speed, tri-color marking for precision, and +compaction to tidy up the mess. Together, these techniques ensure that memory is used efficiently without bogging down +your app. + +You don’t need to be a GC expert to write clean Ruby code, but knowing how it works can give you an edge — whether +you’re optimizing performance, tracking down memory issues, or just curious about what’s going on under the hood. So, +next time you hit that GC.start, you will have all the knowledge to understand what’s going on![^1] + +[^1]: Special kudos to [Athoune](https://blog.garambrogne.net/) and [Chakiral](https://mastodon.social/@Chakiral@hostux.social) for the awesome and valuable review on all the articles of this series!