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.

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 1In 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 2Here, 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 deallocatedIn 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 reclaimedIn 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.
