avatarSaverio Mazza

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

4642

Abstract

short-lived and get garbage-collected quickly. On the other hand, if an object has survived a garbage collection cycle, it’s more likely to be long-lived, so it’s moved to an older generation to be checked less frequently.</p><p id="8ca3">Since <code>persistent_object</code> is still in use (it's being referenced somewhere in the code), its reference count is not zero, and it survives the garbage collection process for Generation 0. That's why it gets moved to Generation 1.</p><h2 id="cc15">Generation 2: Objects that Survived More Than One Garbage Collection Cycle</h2><p id="fbc3">Objects that continue to survive garbage collection cycles eventually move to Generation 2.</p><div id="330e"><pre><span class="hljs-comment"># Create another persistent object</span> very_persistent_object = (1, 2, 3)

<span class="hljs-comment"># Run garbage collection on Generations 0 and 1</span> gc.collect(0) gc.collect(1)

<span class="hljs-comment"># At this point, 'very_persistent_object' survives and should move to Generation 2</span></pre></div><p id="21af">Here, <code>very_persistent_object</code> survives garbage collections in both Generation 0 and Generation 1, so it will move to Generation 2.</p><p id="b506">In practice, you usually don’t need to control or monitor these generations manually; Python’s garbage collector handles them automatically. But understanding how these work can be beneficial for debugging and optimization.</p><h1 id="85ef">Rust: Ownership and Borrowing</h1><p id="8f51">Rust’s approach to memory management is fundamentally different from garbage-collected languages like Python. It relies on the concepts of “ownership” and “borrowing” to ensure that resources are managed safely.</p><h2 id="249a">Ownership</h2><p id="34c9">In Rust, every value has a single “owner,” and the value lives as long as its owner does. When the owner goes out of scope, the value and its resources are automatically deallocated. This removes the need for a separate garbage collection process.</p><p id="c2d5">Here’s a quick example:</p><div id="2a77"><pre><span class="hljs-keyword">fn</span> <span class="hljs-title function_">main</span>() { <span class="hljs-keyword">let</span> <span class="hljs-variable">s1</span> = <span class="hljs-type">String</span>::<span class="hljs-title function_ invoke__">from</span>(<span class="hljs-string">"hello"</span>); <span class="hljs-comment">// s1 is the owner of the value "hello"</span> <span class="hljs-keyword">let</span> <span class="hljs-variable">s2</span> = s1; <span class="hljs-comment">// s1's ownership is transferred to s2</span>

<span class="hljs-comment">// println!("{}", s1);  // This would cause an error because s1 no longer owns the value</span>
<span class="hljs-built_in">println!</span>(<span class="hljs-string">"{}"</span>, s2);  <span class="hljs-comment">// This is fine, s2 is now the owner</span>

} <span class="hljs-comment">// s2 goes out of scope, "hello" is deallocated</span></pre></div><p id="af60">In this example, <code>s1</code> initially owns the string "hello". Ownership is then transferred to <code>s2</code>. When <code>s2</code> goes out of scope at the end of <code>main()</code>, the string "hello" is automatically deallocated.</p><h2 id="1cde">Borrowing</h2><p id="94c8">Sometimes you need to access a value without taking ownership, so Rust allows “borrowing”. You can borrow a value as a mutable or an immutable reference.</p><p id="7cd0">Immutable Borrow:</p><div id="7fc4"><pre><span class="hljs-keyword">fn</span> <span class="hljs-title function_">main</span>() { <span class="hljs-keyword">let</span> <span class="hljs-variable">s1</span> = <span class="hljs-type">String</span>::<span class="hljs-title function_ invoke__">from</span>(<span class="hljs-string">"hello"</span>); <span class="hljs-keyword">let</span> <span class="hljs-variable">len</span> = <span class="hljs-title function_ invoke__">calculate_length</span>(&s1); <span class="hljs-comment">// &s1 borrows s1 without taking ownership</span> <span class="hljs-built_in">println!</span>(<span class="hljs-string">"The length of '{}' is {}."</span>, s1, len); }

<span class="hljs-keyword">fn</span> <span class="hljs-title function_">calculate_length</span>(s: &<span class="hljs-type">String</span>) <span class="hljs-punctuation">-></span> <span class="hljs-type">usize</span> { s.<span class="hljs-title function_ invoke__">len</span>() }</pre></div><p id="6e6c">Mutable Borrow:</p><div id="c3bd"><pre><span class="hljs-keyword">fn</span> <span class="hljs-title function_">main</span>() { <span cla

Options

ss="hljs-keyword">let</span> <span class="hljs-keyword">mut </span><span class="hljs-variable">s1</span> = <span class="hljs-type">String</span>::<span class="hljs-title function_ invoke__">from</span>(<span class="hljs-string">"hello"</span>); <span class="hljs-title function_ invoke__">change_string</span>(&<span class="hljs-keyword">mut</span> s1); <span class="hljs-comment">// &mut s1 borrows s1 as a mutable reference</span> <span class="hljs-built_in">println!</span>(<span class="hljs-string">"{}"</span>, s1); }

<span class="hljs-keyword">fn</span> <span class="hljs-title function_">change_string</span>(s: &<span class="hljs-keyword">mut</span> <span class="hljs-type">String</span>) { s.<span class="hljs-title function_ invoke__">push_str</span>(<span class="hljs-string">", world"</span>); }</pre></div><p id="d75a">In these examples, <code>&s1</code> and <code>&mut s1</code> borrow the value without taking ownership, allowing <code>s1</code> to continue being used after the function calls.</p><p id="0382">The key benefit of Rust’s approach is that it provides precise control over which parts of the code can use, modify, or deallocate values, leading to safer and more efficient programs without the need for garbage collection.</p><h1 id="c5cc">JavaScript: Mark-and-Sweep Algorithm</h1><p id="8913">In JavaScript, the garbage collector uses a technique known as the “mark-and-sweep” algorithm to manage memory. This algorithm helps the language automatically reclaim memory that is no longer in use. The mark-and-sweep approach is different from Python’s reference counting and generational garbage collection. Let’s break it down:</p><h2 id="0d63">Mark Phase</h2><p id="1053">The garbage collection process starts by taking a set of “root” objects. These are usually variables currently in scope, global variables, and other foundational data that’s always reachable. The algorithm then traverses the object graph, “marking” all objects that can be reached directly or indirectly from these roots. Essentially, it marks all objects that are still in use.</p><h2 id="dd62">Sweep Phase</h2><p id="5235">After the mark phase is completed, the garbage collector moves to the sweep phase. In this phase, the memory occupied by all the unmarked objects is reclaimed. These unmarked objects are considered “garbage” since they can no longer be reached or used by the application.</p><p id="aed3">Here’s a conceptual example:</p><div id="0c98"><pre><span class="hljs-comment">// Root object: global variable a</span> <span class="hljs-keyword">var</span> a = { prop1: <span class="hljs-string">"value1"</span>, prop2: <span class="hljs-string">"value2"</span> };

<span class="hljs-comment">// Root object: global variable b</span> <span class="hljs-keyword">var</span> b = { prop: <span class="hljs-string">"value3"</span> };

<span class="hljs-comment">// b is now referencing a</span> b.newProp = a;

<span class="hljs-comment">// Removing the reference from a</span> a = <span class="hljs-literal">null</span>;

<span class="hljs-comment">// Run the garbage collector (this is actually automatic and cannot be forced in JavaScript)</span> <span class="hljs-comment">// 1. Mark: b and everything it references (which now includes what a used to reference)</span> <span class="hljs-comment">// 2. Sweep: Since a is now null and no longer marks its object, that memory can be reclaimed</span></pre></div><p id="1c75">In this example, even though we set <code>a</code> to <code>null</code>, the object originally referenced by <code>a</code> is not garbage because it's still accessible through <code>b.newProp</code>.</p><h2 id="5f87">Lack of Explicit Control</h2><p id="2010">In JavaScript, the timing of when garbage collection occurs is abstracted away from the developer. You don’t have direct control to trigger the garbage collection process, unlike some functionalities in Python where you can explicitly call <code>gc.collect()</code> to run garbage collection.</p><p id="7717">The advantage of the mark-and-sweep approach in JavaScript is that it can identify and collect circular references, something that simple reference counting would struggle with. However, the downside is that you have less predictability regarding when the garbage collection will happen, which might cause performance fluctuations in your application.</p><p id="e563"><a href="https://substack.com/profile/29984465-saverio-mazza?utm_source=profile-page"><i>Subscribe to my newsletter</i></a><i> to get access to all the content I’ll be publishing in the future.</i></p></article></body>

Garbage Collection in Python, Rust, and JavaScript

Memory management is an essential aspect of any programming language, ensuring that resources are used efficiently. While some languages require manual memory management, others automate this process. Python, Rust, and JavaScript each employ unique strategies for garbage collection.

Garbage Collection

Python: Reference Counting & Generational Garbage Collection

Python employs a technique known as “reference counting” for its garbage collection. Each object has a counter that tracks the number of references to it. When this count reaches zero, the object is removed from memory.

In other words, every object in memory has an associated number (called a “reference count”) that keeps track of how many variables or other objects are pointing to it.

import sys

# Create an object x
x = [1, 2, 3]

# Get the reference count of x (should be 1)
print("Reference Count of x:", sys.getrefcount(x) - 1)

# Create a reference to x
y = x

# Reference count increases by 1
print("Reference Count of x after y = x:", sys.getrefcount(x) - 1)

# Delete a reference
del y

# Reference count decreases by 1
print("Reference Count of x after del y:", sys.getrefcount(x) - 1)
Reference Count of x: 1
Reference Count of x after y = x: 2
Reference Count of x after del y: 1

Python employs a generational approach to further improve the efficiency of its garbage collection. Objects are categorized into three different “generations”:

Generation 0: New Objects

Objects are initially allocated in Generation 0. This is the first stage of their lifecycle.

# Import the gc (garbage collection) module
import gc

# Enable debugging to print garbage collection information
gc.set_debug(gc.DEBUG_STATS)

# Create a new list object; this object will initially be in Generation 0
new_object = [1, 2, 3]

# Manually run garbage collection only on Generation 0
gc.collect(0)

When you create new_object, it's a new object, and it will start its life in Generation 0.

Generation 1: Objects that Survived One Garbage Collection Cycle

Objects that are not collected during a garbage collection cycle in Generation 0 move to Generation 1.

# Create a persistent object
persistent_object = {"key": "value"}

# Run garbage collection on Generation 0
gc.collect(0)

# At this point, 'persistent_object' survives and moves to Generation 1

In Python’s generational garbage collection, when an object is first created, it is placed in Generation 0. Whenever a garbage collection cycle runs on this generation, Python looks for objects that are no longer needed (i.e., objects with a reference count of zero) to remove them and free up memory.

If an object like persistent_object survives this garbage collection cycle—meaning it is still being referenced or used—it "ages" and moves to the next generation, in this case, Generation 1.

The idea behind this is that newly created objects are more likely to be short-lived and get garbage-collected quickly. On the other hand, if an object has survived a garbage collection cycle, it’s more likely to be long-lived, so it’s moved to an older generation to be checked less frequently.

Since persistent_object is still in use (it's being referenced somewhere in the code), its reference count is not zero, and it survives the garbage collection process for Generation 0. That's why it gets moved to Generation 1.

Generation 2: Objects that Survived More Than One Garbage Collection Cycle

Objects that continue to survive garbage collection cycles eventually move to Generation 2.

# Create another persistent object
very_persistent_object = (1, 2, 3)

# Run garbage collection on Generations 0 and 1
gc.collect(0)
gc.collect(1)

# At this point, 'very_persistent_object' survives and should move to Generation 2

Here, very_persistent_object survives garbage collections in both Generation 0 and Generation 1, so it will move to Generation 2.

In practice, you usually don’t need to control or monitor these generations manually; Python’s garbage collector handles them automatically. But understanding how these work can be beneficial for debugging and optimization.

Rust: Ownership and Borrowing

Rust’s approach to memory management is fundamentally different from garbage-collected languages like Python. It relies on the concepts of “ownership” and “borrowing” to ensure that resources are managed safely.

Ownership

In Rust, every value has a single “owner,” and the value lives as long as its owner does. When the owner goes out of scope, the value and its resources are automatically deallocated. This removes the need for a separate garbage collection process.

Here’s a quick example:

fn main() {
    let s1 = String::from("hello");  // s1 is the owner of the value "hello"
    let s2 = s1;  // s1's ownership is transferred to s2

    // println!("{}", s1);  // This would cause an error because s1 no longer owns the value
    println!("{}", s2);  // This is fine, s2 is now the owner
}  // s2 goes out of scope, "hello" is deallocated

In this example, s1 initially owns the string "hello". Ownership is then transferred to s2. When s2 goes out of scope at the end of main(), the string "hello" is automatically deallocated.

Borrowing

Sometimes you need to access a value without taking ownership, so Rust allows “borrowing”. You can borrow a value as a mutable or an immutable reference.

Immutable Borrow:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);  // &s1 borrows s1 without taking ownership
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

Mutable Borrow:

fn main() {
    let mut s1 = String::from("hello");
    change_string(&mut s1);  // &mut s1 borrows s1 as a mutable reference
    println!("{}", s1);
}

fn change_string(s: &mut String) {
    s.push_str(", world");
}

In these examples, &s1 and &mut s1 borrow the value without taking ownership, allowing s1 to continue being used after the function calls.

The key benefit of Rust’s approach is that it provides precise control over which parts of the code can use, modify, or deallocate values, leading to safer and more efficient programs without the need for garbage collection.

JavaScript: Mark-and-Sweep Algorithm

In JavaScript, the garbage collector uses a technique known as the “mark-and-sweep” algorithm to manage memory. This algorithm helps the language automatically reclaim memory that is no longer in use. The mark-and-sweep approach is different from Python’s reference counting and generational garbage collection. Let’s break it down:

Mark Phase

The garbage collection process starts by taking a set of “root” objects. These are usually variables currently in scope, global variables, and other foundational data that’s always reachable. The algorithm then traverses the object graph, “marking” all objects that can be reached directly or indirectly from these roots. Essentially, it marks all objects that are still in use.

Sweep Phase

After the mark phase is completed, the garbage collector moves to the sweep phase. In this phase, the memory occupied by all the unmarked objects is reclaimed. These unmarked objects are considered “garbage” since they can no longer be reached or used by the application.

Here’s a conceptual example:

// Root object: global variable `a`
var a = { 
  prop1: "value1",
  prop2: "value2"
};

// Root object: global variable `b`
var b = { 
  prop: "value3"
};

// b is now referencing a
b.newProp = a;

// Removing the reference from `a`
a = null;

// Run the garbage collector (this is actually automatic and cannot be forced in JavaScript)
// 1. Mark: `b` and everything it references (which now includes what `a` used to reference)
// 2. Sweep: Since `a` is now null and no longer marks its object, that memory can be reclaimed

In this example, even though we set a to null, the object originally referenced by a is not garbage because it's still accessible through b.newProp.

Lack of Explicit Control

In JavaScript, the timing of when garbage collection occurs is abstracted away from the developer. You don’t have direct control to trigger the garbage collection process, unlike some functionalities in Python where you can explicitly call gc.collect() to run garbage collection.

The advantage of the mark-and-sweep approach in JavaScript is that it can identify and collect circular references, something that simple reference counting would struggle with. However, the downside is that you have less predictability regarding when the garbage collection will happen, which might cause performance fluctuations in your application.

Subscribe to my newsletter to get access to all the content I’ll be publishing in the future.

Garbage Collection
Python
Rust
JavaScript
Memory Management
Recommended from ReadMedium