How Do Memory Allocators Manage Free Lists and Fragmentation?

Dynamic memory allocators manage heap memory by tracking unallocated blocks using data structures known as free lists and applying placement, splitting, and coalescing strategies to minimize external fragmentation. External fragmentation occurs when free memory is broken into many small, non-contiguous blocks, preventing a large allocation request from succeeding even when the total free memory is sufficient. Classical allocators balance allocation speed, metadata overhead, and memory utilization through specialized free list organizations and block-merging techniques.

Free List Architectures

A free list is a data structure that records the location and size of every unallocated block on the heap. Allocators typically embed this metadata directly within the free blocks themselves, avoiding the need for separate auxiliary memory.

Implicit Free Lists

In an implicit free list, the allocator stores a header—and often a footer—in every block, whether allocated or free. The header contains the block's total size and an allocation bit indicating whether the block is in use. Because there are no explicit pointers connecting free blocks, the allocator must traverse the entire heap sequentially from the beginning to find a suitable free block. While simple and space-efficient for small heaps, implicit lists have a linear search time of \(O(N)\), where \(N\) is the total number of blocks on the heap.

Explicit Free Lists

An explicit free list improves lookup performance by storing next and prev pointers inside the payload area of each free block, forming a doubly linked list of only the unallocated blocks. This reduces search time to \(O(F)\), where \(F\) is the number of free blocks. When a block is freed, it can be inserted into the list using either a Last-In, First-Out (LIFO) policy—inserting at the head in \(O(1)\) time—or an address-ordered policy, which keeps the list sorted by memory address to simplify coalescing.

Segregated Free Lists

Segregated free lists partition free blocks into multiple lists based on size classes, such as powers of two (\(2^k\)). An array of list heads points to each size class. When a request of size \(s\) arrives, the allocator computes the appropriate size class index and searches only that list (or the next larger non-empty list). This approach approximates best-fit placement in near-constant time, \(O(1)\), and is widely used in production allocators like jemalloc and ptmalloc.

Placement Policies

When searching a free list for a block of size \(s\), the allocator uses a placement policy to choose among available candidates:

  • First Fit: Scans the list from the beginning and selects the first free block that is large enough. It is fast but can leave small, unusable splinters near the front of the list.
  • Next Fit: Resumes scanning from the point where the previous search ended. This avoids clustering splinters at the start of the heap, though it can slightly increase fragmentation compared to first fit.
  • Best Fit: Searches the entire list (or size class) to find the smallest free block that satisfies the request. This minimizes wasted space per allocation but can be slower and may produce tiny leftover fragments.

Combating External Fragmentation

Even with optimal placement, repeated allocations and deallocations inevitably divide the heap into small, scattered chunks. Allocators use several mechanisms to restore contiguous free space.

Block Splitting

When an allocator selects a free block of size \(B\) to satisfy a request of size \(s\), and \(B - s\) is large enough to hold a valid block (including its header, footer, and minimum payload), the allocator splits the block into two. The first part of size \(s\) is marked allocated, and the remainder of size \(B - s\) is returned to the free list. Without splitting, internal fragmentation would waste the unused portion of the block.

Coalescing with Boundary Tags

Coalescing is the process of merging adjacent free blocks into a single larger block. To perform coalescing in \(O(1)\) time without traversing the entire heap, allocators use boundary tags. Each block includes both a header at the start and a footer (a copy of the header) at the end. When a block is freed, the allocator checks the allocation bit of the immediately preceding block (via its footer) and the immediately following block (via its header). If either neighbor is free, their sizes are combined into one contiguous free block.

Deferred Coalescing and Compaction

Immediate coalescing merges blocks on every free() call, which can cause thrashing if a program repeatedly allocates and frees blocks of the same size. Some allocators use deferred coalescing, waiting until an allocation request fails or a threshold is reached before scanning and merging adjacent free blocks. In environments with managed runtimes or garbage collection, allocators may also perform heap compaction—relocating live objects to one end of the heap to eliminate external fragmentation entirely—though this is generally not possible in unmanaged languages like C and C++ where raw pointers cannot be safely updated.