Deep dive into the Zend Memory Manager: Understanding PHP’s Internal Memory Management
Memory management is a fundamental aspect of any high-performance programming language. In the case of PHP, this responsibility falls to the Zend Memory Manager (ZMM), a sophisticated component of the Zend Engine that powers PHP.
This article explores the internal mechanisms of this memory manager by analyzing its implementation in the zend_alloc.c file, with a particular focus on “small allocations.”
Custom Needs? Custom Allocator!
Before diving into the technical details, it’s reasonable to ask: why doesn’t PHP simply use the standard system memory allocation functions (malloc, free, etc.)?
The answer comes down to several key points:
- Performance: PHP applications constantly create and destroy objects, variables, and structures. A system optimized for these specific operations is much more efficient.
- Request Lifecycle: PHP typically operates on a model where each web request starts with a clean memory state and releases all memory at the end. This specific model allows for targeted optimizations.
- Memory Leak Detection: A custom memory manager can integrate tools to track allocations and identify leaks.
- Fragmentation Control: By directly managing allocations, PHP can minimize memory fragmentation, a common issue in long-running applications.
These constraints led to the creation of the Zend Memory Manager (ZMM). Dmitry Stogov (dstogov) was the primary architect behind this system, and many of ZMM’s improvements can be credited to his work.
Let’s take a closer look…
Fundamental Architecture of the Zend Memory Manager
The ZMM is structured around a few core concepts:

To minimize direct calls to system allocation functions and reduce fragmentation, the ZMM organizes memory into three levels (which also represent three approaches in memory management development):
- Memory Pools: Large memory regions (chunks) are preallocated to reduce malloc calls.
- Bucketing Memory: These chunks are divided into pages, which are then further split into buckets (or bins) that group fixed-size blocks.
- Fixed-Size Allocation: Each small allocation is made within a predefined block, simplifying the management of frequent allocation requests.
🤓 Don’t expect to find much theory on “bucketing memory” — it’s a concept used in highly specific system programming and memory allocation contexts. The internet seems rather scarce on this topic! For the more curious, here are some foundational papers on “bucketized allocations”:
- Fast Allocation and Deallocation of Memory Based on Object Lifetimes by David R. Hanson
- Dynamic Storage Allocation: A Survey and Critical Review by Paul R Wilson Mark S Johnstone Michael Neely and David Boles
A Focus on the Heap
At the core of the ZMM lies the concept of the heap. In the code, the _zend_mm_heap structure represents the global state of the memory manager (simplified extract):
struct _zend_mm_heap {
zend_mm_storage *storage;
size_t size; /* current memory usage */
size_t peak; /* peak memory usage */
uintptr_t shadow_key; /* free slot shadow ptr xor key */
zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */
size_t real_size; /* current size of allocated pages */
size_t real_peak; /* peak size of allocated pages */
size_t limit; /* memory limit */
int overflow; /* memory overflow flag */
zend_mm_huge_list *huge_list; /* list of huge allocated blocks */
zend_mm_chunk *main_chunk;
zend_mm_chunk *cached_chunks; /* list of unused chunks */
int chunks_count; /* number of allocated chunks */
int peak_chunks_count; /* peak number of allocated chunks for current request */
int cached_chunks_count; /* number of cached chunks */
double avg_chunks_count; /* average number of chunks allocated per request */
int last_chunks_delete_boundary; /* number of chunks after last deletion */
int last_chunks_delete_count; /* number of deletion over the last boundary */
pid_t pid;
zend_random_bytes_insecure_state rand_state;
};
The main heap contains information about total allocated memory, peak usage, and other useful metrics for monitoring memory consumption.
The memory_limit concept is therefore monitored by the heap.
Chunk and Page Strategy
The ZMM employs a hierarchical approach to manage memory:
- Chunks: Large memory blocks (typically 2MB) are allocated directly from the operating system.
- Pages: Each chunk is divided into pages (usually 4KB), which act as intermediate allocation units.
- Arenas: An arena allocator is a system where a large contiguous memory block (arena) is preallocated and then subdivided as needed. Allocations within an arena are not freed individually; instead, the entire arena is cleared in a single operation (e.g., at the end of a PHP request, “request-bound”). This approach eliminates the need to track each allocation and reduces memory management overhead. In the ZMM, the arena concept is implemented through chunks and pages (see zend_arena.h).
- Small Allocations: For small-sized requests, ZMM uses free lists or free slot lists (linked lists of available slots) categorized by size.
- Large Allocations: Requests for large memory blocks (≥ 2MB) are allocated directly using mmap, thus delegating them to the OS.
Small Allocations: The Core of Optimization
When PHP needs to allocate a small amount of memory (typically less than 3KB), the ZMM follows these steps:

Step 1 : The bins table and the mapping size → bin
The file zend_alloc_sizes.h defines a data table using the macro ZEND_MM_BINS_INFO. Each row in this table contains several pieces of information for a given bin (or bucket):
- Index (count) – A numeric identifier (ranging from 0 to 29 in this example) used as an index in an internal array.
- Block Size (size) – The fixed size of blocks in this bin (e.g., 8, 16, 24, …, 3072 bytes).
- Number of Slots (nb_slots) – The number of blocks of this size that can fit into a page.
- Number of Pages (nb_pages) – The number of pages allocated when a new group of blocks needs to be created for this bin.
For an allocation request, the ZMM must determine which bin (or bucket) corresponds to the requested size.
To achieve this, a mapping system returns the index of the bin whose block size is greater than or equal to the requested size.
For example, if a request asks for 100 bytes, the memory manager looks up the table to find the first bin with a block size of at least 100 bytes. The table’s structure allows for an efficient lookup, sometimes using binary search or a predefined index array.
In the ZMM, there are 30 predefined buckets (bins) for small allocations.
// Zend/zend_alloc.c#L218
/* Small Alloication predifined buckets sizes
#define ZEND_MM_BINS 30
// Zend/zend_alloc_sizes.h
/* Each bucket represents size classes, ranging from 8 bytes to approximately 3KB. */
#define ZEND_MM_BINS_INFO(_, x, y)
_( 0, 8, 512, 1, x, y)
_( 1, 16, 256, 1, x, y)
_( 2, 24, 170, 1, x, y)
_( 3, 32, 128, 1, x, y)
_( 4, 40, 102, 1, x, y)
_( 5, 48, 85, 1, x, y)
_( 6, 56, 73, 1, x, y)
// ...
_(27, 2048, 8, 4, x, y)
_(28, 2560, 8, 5, x, y)
_(29, 3072, 4, 3, x, y)
For example:
- The line _(6, 56, 73, 1, x, y) indicates that in a 4096KB page, there will be 73 slots of 56 bytes, corresponding to bucket (bin) 7 (index 6).
- The line _(27, 2048, 8, 4, x, y) indicates that 4 pages of 4096KB (totaling 16MB) will contain 8 slots of 2048 bytes, corresponding to bucket (bin) 28 (index 27).
Step 2: Accessing the Free List and Retrieving a Block
Once the bin is selected, the memory manager refers to the free list associated with that bin. Here’s how it works in detail:
🚀 The Free List
For each bin, the manager maintains a linked list (free_list) containing previously allocated memory blocks that have been freed.
- These lists enable quick memory reuse without needing to allocate from the arena again.
- The structure of an allocated block in a bin generally includes a small header (or an implicit pointer) storing a reference to the next block in the free list.
🏗️ Access and Block Extraction
If the free list is not empty:
- The manager pops the first block from the list and returns it to satisfy the allocation request.
If the free list is empty:
- The manager allocates a new memory group from the arena allocator.
- The total allocated size is determined by nb_pages × ZEND_MM_PAGE_SIZE.
- This new memory block is then split into multiple fixed-size blocks corresponding to the bin.
- The remaining blocks (or all except the one used for the current request) are inserted into the free list for future reuse.
In the zend_alloc.c file, this mechanism can be observed in functions like zend_mm_alloc_small.
static zend_always_inline void *zend_mm_alloc_small(zend_mm_heap *heap, size_t size, bin_num_t bin_num)
{
zend_mm_free_slot *p = heap->free_slot[bin_num];
if (UNEXPECTED(p == NULL)) {
/* If no bloc is available, a new page is allocated (arena) */
return zend_mm_alloc_small_slow(heap, bin_num);
}
/* Use an existing bloc from the free list */
heap->free_slot[bin_num] = p->next_free_slot;
return (void*)p;
}
The function zend_mm_alloc_small_slow is called when no free block is available.
static void *zend_mm_alloc_small_slow(zend_mm_heap *heap, bin_num_t bin_num)
{
/* Require a new page from the pages manager */
zend_mm_chunk *chunk = heap->main_chunk;
zend_mm_page_info *info = chunk->map + page_num;
/* ... allocate a new page into the arena ... */
/* Split the page into small blocs of bin_num size */
zend_mm_free_slot *p = (zend_mm_free_slot*)ZEND_MM_PAGE_ADDR(chunk, page_num);
int i = 0;
/* Intialize the chain of free blocs */
while (i < bin_data_size[bin_num] / bin_size[bin_num] - 1) {
p->next_free_slot = (zend_mm_free_slot*)((char*)p + bin_size[bin_num]);
p = p->next_free_slot;
i++;
}
p->next_free_slot = NULL;
/* ... */
return first_block;
}
Concrete example:
Take the following line from table:
// Zend/zend_alloc_sizes.h#L53
_(20, 640, 32, 5, x, y)
- Block Size: 640 bytes.
- Number of Slots: 32 blocks per group.
- Number of Pages: 5 pages of 4 KB, totaling 5 × 4096 = 20 KB.
Here, the entire 20 KB space is used to provide exactly 32 blocks of 640 bytes (32 × 640 = 20480 bytes).
When bin 20 is selected for an allocation request of 640 bytes or less, the memory manager checks the free list associated with this bin.
- If a free block is available, it is removed from the list.
- Otherwise, a new 20 KB memory region is allocated, divided into 32 blocks, and one of them is used immediately, while the remaining ones are added to the free list.
This approach, called “segregated free list”, is highly efficient for languages like PHP, which performs numerous small allocations in succession.
Benefits:
✅ Fast access to a given bin when PHP requests an allocation.
✅ Efficient lookup through the bin table.
✅ Optimized block reuse to minimize memory fragmentation.
This method significantly reduces system calls and memory fragmentation, leading to better performance.
Memory Deallocation and Reuse
When you call unset() on a PHP variable or when an object is no longer referenced, the ZMM handles memory deallocation. Several optimizations are implemented:
- Freed small blocks are placed in the free list of their corresponding size class (buckets or bins), making them immediately available for reuse.
- If an entire page becomes free, it can be returned to its parent chunk.
- If a chunk is no longer in use, it can be returned to the operating system.
Memory Corruption Protection
To enhance the integrity of the linked list and detect potential memory corruption, the “shadow pointer” technique is used:
- In addition to storing a pointer to the next free block at the beginning of the block, a copy of this pointer is also stored at the end of the same block.
- However, this copy is not stored as is. It is encoded using XOR with a random key (heap->shadow_key) specific to the heap. The encoded pointer is then converted to big-endian format. This conversion ensures that corruptions affecting the least significant bytes (most prone to alteration) manifest in the most significant bytes, increasing the chances of detecting invalid addresses.
- Before dereferencing a pointer to the next free block, the memory manager decodes the shadow pointer by applying inverse XOR with the random key. It converts the result back to the machine’s native byte order and validates the decoded pointer against the original pointer stored at the beginning of the block. If the two pointers do not match, memory corruption is detected.

This technique belongs to a broader family of memory error detection approaches. Similar concepts exist in other systems:
- Stack or heap “canaries” — Sentinel values placed at memory block boundaries.
- Guard pages — Protected memory pages inserted between allocations.
- Checksums and validation metadata — Used to verify data integrity.
Keep in mind that shadow pointers are neither a unique invention of PHP nor a universal standard practice, but rather a specific technique that PHP has adapted and integrated into its memory management model to suit its unique needs.
Summary: Advantages of this System for Small Allocations
- Fewer system calls: A single request to the operating system retrieves an entire page, which can serve multiple small allocations.
- Spatial locality: Allocations of the same size are grouped together, improving CPU cache performance.
- Reduced fragmentation: Dividing a page into uniform-sized blocks eliminates internal fragmentation within that page.
- Faster allocation: Once an arena (chunk + page) is available, allocations become simple pointer operations, which are much faster than a malloc call.
- Efficient mass deallocation: At the end of a PHP request, freeing entire chunks is significantly more efficient than individually deallocating small blocks.

And that’s it!
Now you know a bit more about PHP’s internals, especially memory allocation with the ZMM. But your journey doesn’t stop here, you’ve only seen part of the picture. I didn’t go into detail about RUNS, we didn’t explore the famous ZVALs, or the various allocation HEADERS, etc…
It’s a fascinating and complex topic, and often, quite a brain teaser!
Have fun 😉
🎁 A little gift if you’ve read this far: Here’s a stat you can drop at a party to impress everyone:
Internal fragmentation in the ZMM is usually < 25%.
You might think that’s “a lot”.
Well, yes, if you’re working on embedded systems, but we’re coding for the web, so it’s less impactful. It’s always about trade-offs. The ZMM does everything for us, so it can’t be perfect!
🎁 Otherwise, you can always code your own allocator… Oh, didn’t I tell you that the PHP HEAP structure lets you throw in your own custom allocator? Curious… 😏
Check it out here : https://github.com/php/php-src/blob/master/Zend/zend_alloc.c#L3108
// In your amazing custom extension
void set_custom_allocator() {
zend_mm_set_custom_handlers(zend_mm_get_heap(), &custom_handlers);
}
If you’ve made it this far, a big thank you to you! ❤️ ❤️
For more updates, visit ekino’s website and follow us on LinkedIn.
Deep dive into the Zend Memory Manager : Understanding PHP’s Internal Memory Management was originally published in ekino-france on Medium, where people are continuing the conversation by highlighting and responding to this story.