Unveiling malloc: How C Allocates Memory Behind the Scenes
This comprehensive guide explains the inner workings of the C malloc function, covering memory layout, stack vs heap, glibc memory pools, system calls like brk/sbrk and mmap, allocation strategies, fragmentation, alignment, multithreading safety, and comparisons with calloc, realloc, and C++ new.
In C and low‑level development, malloc is the core entry for dynamic memory allocation, yet many developers only use it without understanding how memory is actually allocated.
1. Basics of Memory Layout
A typical C program’s address space consists of several segments:
Code segment : read‑only compiled instructions.
Data segment : global and static variables (initialized and BSS).
Stack segment : automatic local variables, function parameters, return addresses; managed in LIFO order.
Heap segment : memory obtained via malloc, calloc, realloc, and released with free.
Understanding this layout is essential before diving into malloc.
2. Introducing malloc
mallocis declared in <stdlib.h> as void* malloc(size_t size). It returns a pointer to size bytes or NULL on failure. The pointer must be cast to the appropriate type before use.
Typical Use Cases
Dynamic arrays : allocate an array when the size is unknown at compile time and resize with realloc.
Linked lists : allocate each node with malloc and link them.
Complex structures (trees, graphs) : allocate nodes on demand.
3. Interaction with the Operating System
glibc Memory Pool
On Linux, malloc does not call the kernel directly for every request. Instead, glibc maintains a user‑space memory pool. If the pool has a suitable free block, malloc returns it without a system call, greatly reducing overhead.
brk / sbrk
When the pool lacks space, malloc may invoke brk (or its wrapper sbrk) to extend the program’s data segment. This moves the program break upward, allocating contiguous virtual memory pages on demand.
mmap
For large allocations (typically >128 KB), malloc prefers mmap to create an independent anonymous mapping. mmap avoids heap fragmentation and allows the kernel to release the region with munmap when freed.
4. malloc’s Internal Mechanisms
Memory Pools
glibc reserves a large chunk of memory from the kernel at startup (e.g., 1 MiB) and serves small allocations from this pool, similar to a supermarket’s inventory.
Free‑Block Management
Two common schemes are used:
Free‑list : a linked list of free blocks; allocation scans the list (first‑fit, best‑fit) and may split a larger block.
Buddy system : blocks are powers of two; splitting and merging maintain alignment and reduce external fragmentation.
Slab allocator : pre‑creates slabs of fixed‑size objects for high‑frequency allocations (e.g., kernel inode structures).
Allocation Flow
Check the memory pool for a suitable free block.
If none, search the free‑list or buddy system.
If still none, invoke brk for small requests or mmap for large ones.
Align the returned address to the platform’s alignment requirement (4 bytes on 32‑bit, 8 bytes on 64‑bit).
Return the pointer; on failure, return NULL.
free() Flow
Mark the block as free.
Attempt to merge with adjacent free blocks (or buddy).
If the block belongs to an mmap region and meets size thresholds, call munmap to return memory to the kernel; otherwise, keep it in the pool.
5. Optimization Strategies
Allocation Policies
Small allocations (≤128 KB) use brk and the internal pool, while large allocations (>128 KB) use mmap to avoid heap fragmentation and enable immediate release.
Alignment
malloc ensures the returned address satisfies the strictest alignment among the requested type. For a struct containing char, int, and short, the overall alignment is 4 bytes, leading to padding that increases the struct size from 7 bytes to 12 bytes.
Fragmentation Reduction
Free‑list merging, buddy coalescing, and occasional heap compaction (rare due to pointer‑relocation complexity) mitigate internal and external fragmentation.
6. malloc in Multithreaded Environments
Thread‑Safety Issues
Concurrent malloc / free calls can corrupt global data structures, cause race conditions, and produce inconsistent views due to CPU caches.
Solutions
Global mutex : protect the allocator with a pthread_mutex_t. Simple but can become a bottleneck.
Spinlock : busy‑wait for short critical sections, avoiding context switches.
Thread‑Local Cache (TLC) : each thread maintains a private pool; only when the local cache is empty or full does it lock the global pool.
Best Practices
Always check the return value of malloc before dereferencing.
Pair every malloc with a matching free to avoid leaks.
Initialize allocated memory (e.g., with memset or calloc) before use.
After free, set the pointer to NULL to prevent dangling pointers.
7. Comparing malloc with Other Allocation Functions
malloc vs. calloc
mallocallocates uninitialized memory; calloc allocates and zero‑fills it. calloc takes two arguments (element count and size) and is convenient when zero‑initialization is required.
malloc vs. realloc
reallocresizes an existing allocation, either expanding in place if adjacent free space exists or allocating a new block, copying data, and freeing the old block.
malloc vs. C++ new
newallocates from the C++ free store, automatically calls constructors, returns a typed pointer, and throws std::bad_alloc on failure (unless nothrow is used). malloc returns void*, requires explicit casts, does not invoke constructors, and signals failure with NULL.
When working with C++ objects, new should be preferred; malloc remains useful for raw buffers or interfacing with C libraries.
Deepin Linux
Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
