Skip to Content
Course content

248: The gc Module and Garbage Collection Internals

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably internalised the idea that Python handles memory for you. For the most part, it does. You create an object, you stop using it, and it vanishes. But there's a specific scenario where Python's primary mechanism—reference counting—completely fails. I want to show you exactly how that happens and how the gc module steps in to save us.

The Reference Counting Trap

Let's start with a simple scenario. I'll create two objects that point to each other. This happens all the time in real-world code, like in a doubly-linked list or a parent-child relationship in a GUI tree.

import gc
import sys

class Node:
    def __init__(self, name):
        self.name = name
        self.link = None
    def __del__(self):
        print(f"Cleaning up {self.name}")

# Create a circular reference
a = Node("Node A")
b = Node("Node B")
a.link = b
b.link = a

# Now, let's "delete" them from our local scope
del a
del b
print("Local variables deleted.")

If you run this, you'll notice something frustrating: the __del__ methods never fire. I've deleted the variables a and b, so why are the objects still alive? It's because Node A still has a reference to Node B, and Node B still has one to Node A. Their reference counts never hit zero. They are effectively ghosts—unreachable by your code, but still occupying RAM.

Forcing a Cleanup

This is where the gc (Garbage Collector) module comes in. While reference counting is the first line of defense, the cyclic garbage collector is the backup. It doesn't run every second; it triggers based on thresholds. I can, however, force it to run right now to see if it can spot these orphans.

# Continuing from the previous snippet...
print("Manually triggering GC...")
unreachable_count = gc.collect()
print(f"GC collected {unreachable_count} objects.")

Now, the __del__ messages finally print. By calling gc.collect(), I told Python to stop relying on reference counts and instead perform a "cycle detection" scan. It looks for groups of objects that are reachable from each other but unreachable from the "root" of the program. I'll be honest: in a small script, you'll never notice this. But in a long-running server process, these "ghost" objects can lead to a slow, agonizing memory leak.

Peeking at the Garbage Collector's To-Do List

I'm curious about what the GC is actually tracking. We can use gc.get_objects() to see every object Python is currently monitoring. Warning: this list is huge because it includes everything from internal modules to every single string you've defined. To make it useful, I'll filter it.

# Let's create another cycle
x = Node("X")
y = Node("Y")
x.link = y
y.link = x

# Find all Node objects currently in memory
all_nodes = [obj for obj in gc.get_objects() if isinstance(obj, Node)]
print(f"Found {len(all_nodes)} Node objects in memory.")

This is a powerful debugging tool. If you suspect a memory leak, you can snapshot gc.get_objects(), perform an action in your app, snapshot again, and see which objects are growing in number without being cleaned up.

The Hierarchy of Garbage

Python doesn't scan every single object every time the GC runs—that would be a performance nightmare. Instead, it uses "generations." New objects start in Generation 0. If they survive a GC collection, they get promoted to Generation 1, and eventually Generation 2.

The logic is simple: most objects die young. Why spend CPU cycles scanning a long-lived configuration object over and over again? Let's look at the current thresholds:

print(gc.get_threshold())
# Output is usually (700, 10, 10)

This means when the number of allocations minus deallocations exceeds 700, Gen 0 is collected. If Gen 0 has been collected 10 times, Gen 1 is collected, and so on. If you're writing a high-performance application and you notice "stutters" in your execution, it might be the GC hitting a Gen 2 collection. You can actually tune these values using gc.set_threshold(), though I'd suggest leaving them alone unless you have a very specific reason to touch them.




📋 Practical Task

Exercise: Detecting and Breaking Circular Reference Leaks in a Graph Structure

You are building a simple Graph data structure where nodes have a list of neighbors. Because the edges are bidirectional, circular references are inevitable.

  1. Create a GraphNode class with a name and a neighbors list. Implement a __del__ method that prints when a node is destroyed.
  2. Create three nodes (A, B, and C) and connect them in a triangle (A→B, B→C, C→A).
  3. Delete the local references to A, B, and C using del.
  4. Verify that the nodes are not deleted immediately.
  5. Use the gc module to manually trigger a collection and prove that the circular references were identified and cleaned up.
  6. Print the total number of unreachable objects found during that specific collection.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.