How Python Memory Arenas Allocate Memory via malloc

Python manages small-object memory through a specialized allocator within CPython called PyMalloc, which organizes memory hierarchically into arenas, pools, and blocks to minimize overhead. At the top of this hierarchy sits the arena—a large, contiguous chunk of memory (typically 256 KiB) requested directly from the operating system using standard C library calls such as malloc or virtual memory mappings. This article explains how Python's arena mechanism requests system memory via malloc, how it subdivides that memory to handle runtime objects, and how it eventually releases that memory back to the operating system.

The CPython Memory Hierarchy

CPython splits memory management into distinct layers depending on the size of the allocation request:

  1. Large allocations (> 512 bytes): Python bypasses its internal custom allocator and delegates directly to the standard C library's malloc (or PyMem_RawMalloc).
  2. Small allocations (≤ 512 bytes): Python routes requests to PyMalloc. PyMalloc serves these allocations out of pre-allocated chunks arranged in three levels:
    • Blocks: The smallest unit, ranging from 8 to 512 bytes in increments of 8 bytes.
    • Pools: 4 KiB memory pages containing uniform-sized blocks.
    • Arenas: 256 KiB structures containing 64 pools each.

Requesting System Memory via Arenas

When Python requires memory for small objects and no existing pool has available space, PyMalloc requests a new arena.

The arena allocator relies on an underlying structure defined in CPython's source code (obmalloc.c) as struct arena_object. The allocation process proceeds as follows:

  1. Arena Object Tracking: Python maintains an array of arena_object descriptors. If an unused descriptor is unavailable, Python expands the descriptor table using standard system allocation.
  2. Invoking the System Allocator: To back the arena descriptor with actual contiguous memory, Python calls _PyObject_Arena.alloc, which resolves to standard system calls. On modern POSIX systems, this can be mmap for anonymous memory, but historically and on many platforms, it uses standard C malloc():
    p = malloc(ARENA_SIZE); /* ARENA_SIZE is typically 256 KiB (262,144 bytes) */
  3. Address Alignment: Pools inside an arena must be strictly aligned to pool-size boundaries (4 KiB) for PyMalloc to compute pool addresses quickly using bitwise masking. Because standard malloc does not guarantee 4 KiB alignment, PyMalloc either allocates slightly more memory to align the base pointer manually or uses platform-specific aligned allocators (such as posix_memalign or VirtualAlloc on Windows).

Arena States and Management

Arenas are managed through a doubly-linked list sorted by the number of available free pools. An arena exists in one of three states:

Python prioritizes filling partially used arenas before drawing pools from completely empty arenas. This strategy minimizes fragmentation and keeps total system memory usage low.

Releasing Memory Back to the Operating System

Because PyMalloc acquires memory from the system in large 256 KiB chunks, individual object deallocations (using free() semantics) do not immediately return memory to the OS.

Instead, when an object is destroyed:

  1. The corresponding block inside a 4 KiB pool is marked as free.
  2. When all blocks inside a pool become free, the entire pool is marked as free within its parent arena.
  3. Only when all 64 pools inside an arena become completely empty can Python deallocate the entire 256 KiB block.

At that point, Python invokes standard free() (or munmap) on the memory pointer held by the arena_object. The descriptor is then returned to the unused arena pool, reducing the process's resident set size (RSS) and returning the physical memory to the operating system.