What Happens When You Delete Large Python Objects?
When large objects are deleted in Python, the memory they occupied is freed from the application level but is not always returned to the operating system immediately. Python utilizes reference counting and an internal memory management system called PyMalloc, working alongside the C runtime allocator. While deleting a large object flags its memory as available for future Python operations, external monitoring tools may still show high process memory usage due to heap fragmentation and allocator retention policies.
Reference Counting and Object Destruction
Python primarily manages memory through reference counting. When you
use the del keyword or an object falls out of scope, Python
decrements that object's reference counter:
import sys
large_list = [i for i in range(10_000_000)]
del large_listThe del statement does not directly wipe memory; it
destroys the name binding and reduces the reference count by one. If the
count reaches zero, Python immediately invokes the object's deallocator
function to free the underlying resources. For objects with circular
references, Python's cyclical garbage collector periodically detects and
clears them during background collection cycles.
PyMalloc vs. The System Allocator
Python distinguishes between small and large memory allocations to optimize performance:
- Small Allocations (≤ 512 bytes): Managed directly by PyMalloc. PyMalloc groups memory into blocks, pools (4 KB), and arenas (256 KB). When a small object is destroyed, its block is returned to its pool. A 256 KB arena is only returned to the operating system if every single pool inside that arena becomes completely vacant.
- Large Allocations (> 512 bytes): Bypasses
PyMalloc entirely. Python routes requests for large objects—such as
large byte strings, long lists, or data structures like NumPy
arrays—directly to standard system-level allocators like the C standard
library's
malloc()ormmap().
Why the Operating System Does Not Reclaim the Memory
Even after a large object is cleared and its memory is passed back
via free(), your system's activity monitor might show that
Python is still using the same amount of RAM. This occurs for several
reasons:
- Allocator Caching: The underlying C library
allocator (such as
glibcon Linux) often holds onto freed virtual memory rather than returning it to the kernel. It assumes the application will request large chunks of memory again soon, saving the performance overhead of repeated system calls. - Heap Fragmentation: Memory allocated on the process
heap via
brk()can only shrink from the top down. If a single small, long-lived object sits at the highest address of the heap, none of the freed memory below it can be unmapped back to the operating system. - Memory Mapping Boundaries: Memory allocated via
mmap()(common for extremely large, contiguous buffers) can typically be returned directly to the kernel viamunmap(). However, if the object was split or allocated via traditional heap expansion, it remains locked in the process space.
Forcing Memory Reclamation
If reclaiming memory is critical to your application's architecture,
standard del commands may need to be paired with more
aggressive cleanup strategies:
- Triggering Garbage Collection: Running
gc.collect()clears objects caught in cyclic references that reference counting missed. - C Library Trimming: On Linux systems running
glibc, you can force the allocator to release unused dynamic memory usingctypes:import ctypes ctypes.CDLL("libc.so.6").malloc_trim(0) - Process Isolation: The most reliable way to
guarantee that all memory from a large computation is returned to the
operating system is to run that task in a separate process using the
multiprocessingmodule. When the child process terminates, the operating system unconditionally reclaims all associated memory pages.