Inside malloc: The glibc Allocator

On this page

glibc malloc is derived from ptmalloc2, but after long-term evolution, you cannot treat the flowcharts from early ptmalloc2 as the complete current implementation. The most useful way to understand it is to distinguish three layers: objects owned by the application, chunks and caches managed by the allocator, and OS mappings and physical residency.

Object Size vs. Chunk Size

malloc receives a request in bytes, but internally it must also satisfy alignment, metadata, and size class requirements. Therefore, the space reserved by the allocator for a 33-byte object may be larger; the difference belongs to internal overhead or internal fragmentation.

Applications can only access the valid object range according to the interface contract. You cannot write out of bounds to metadata just because a particular allocation seemed to provide a bit more space. calloc checks for overflow in the product of element count and size, whereas manually writing malloc(n * size) may already overflow before the call. realloc should preserve the original pointer if it fails for a non-zero target size; do not directly overwrite the only reference. malloc(3)

Expansion Requires Calculation Before Transferring Ownership

realloc may move the object; upon success, the old pointer and any pointers pointing into the interior of the old object must no longer be used. On failure, if the target size is non-zero, the old object remains owned by the caller. The example below combines the multiplication check, the non-zero convention, and temporary pointers into a single runnable example.

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

static int resize_ints(int **items, size_t count) {
    if (count == 0 || count > SIZE_MAX / sizeof **items) return 0;
    int *replacement = realloc(*items, count * sizeof **items);
    if (replacement == NULL) return 0;
    *items = replacement;
    return 1;
}
int main(void) {
    int *items = NULL;
    if (!resize_ints(&items, 2)) return 1;
    items[0] = 7;
    items[1] = 9;
    if (!resize_ints(&items, 4)) { free(items); return 1; }
    if (items[0] != 7 || items[1] != 9) { free(items); return 1; }
    /* The extended part is uninitialized; write before reading. */
    items[2] = 11;
    items[3] = 13;
    int *before_rejection = items;
    if (resize_ints(&items, 0) || items != before_rejection) abort();
    if (sizeof(int) > 1) {
        size_t too_many = SIZE_MAX / sizeof(int) + 1;
        if (resize_ints(&items, too_many) || items != before_rejection) abort();
    }
    printf("%d %d %d %d\n", items[0], items[1], items[2], items[3]);
    free(items);
    return 0;
}

Save as resize-demo.c, compile with cc -Wall -Wextra resize-demo.c -o resize-demo, and the output should be 7 9 11 13. count=0 is explicitly rejected by the interface to avoid mixing the complex realloc(ptr, 0) behaviors across various standards and implementations into this "preserve old object or successfully expand" contract. realloc(3)

This small interface only demonstrates the update of the owning pointer; it does not manage logical length and capacity for the caller. After successful expansion, the caller should also submit the new capacity. If there are steps that may fail, such as element construction, a more complete rollback or commit design is needed. Rejection of zero length and overflow is deterministically reproducible; true allocation failures due to lack of memory should be verified using controlled fault injection, not by gambling on system behavior by requesting a huge object.

tcache and Arena Are Two Layers

Allocator caches existing memory: free does not mean immediate return to OS Thread Cache tcache Fast reuse of suitably sized chunks Arena Free Structures Bins, merging, and top chunk Miss Request More Space from OS brk / mmap; return when conditions met Threads and arenas are not one-to-one; independent mmap allocations, specific cache ranges, and processes depend on the glibc version.

tcache is a per-thread cache; suitable requests can be directly reused from it, reducing the number of times the arena is entered. The arena manages allocation state and free structures over a larger scope, usually with synchronization protection; multiple threads can share one arena, and one arena does not equate to one independent application object.

"The one arena per thread, therefore completely lock-free" is inaccurate. Thread cache hits and arena contention must be measured separately; cross-thread releases also affect which layer an object is cached in and how long it takes to become available again.

The specific sizes tcache can cache, the number of bins, the arena limits, and tuning interfaces vary by version. Newer source code already contains tcache classifications for larger chunks, while some manuals still describe earlier ranges; check the source code and documentation corresponding to your local build. Do not write down rules of thumb like "tens of bytes, 1 KiB, or CPU count times a constant" as invariant rules for all glibc versions.

What Do Free Structures Optimize?

StructureBasic PurposeDistinctions to Keep in Mind
tcacheThread-local reuseNot equal to the arena's own bins
fastbinsFast linking for suitable small chunks, delayed mergingMerging is usually deferred to paths like consolidation
unsorted binGives newly freed blocks a chance for recent reuseNot all frees enter it first
small binsManages blocks for precise size classesClass width and boundaries depend on target configuration
large binsOrganizes lookup by larger sizesCannot be generalized as a pure FIFO queue
top chunkSplittable space at the end of the arenaDifferent from independent holes in bins and independent mmap blocks

The general path is to try suitable caches and existing free space first, splitting, merging, or requesting more from the system when necessary. The actual branch order depends on size and version; you cannot draw a unified flowchart like "first check 128 KiB, above is definitely mmap, below is strictly sequential bin scanning." glibc malloc source code

Who Gets Ownership on free?

In the diagram, addresses identify storage locations, and #numbers identify one lifecycle of an object. After release and reallocation, the address can be the same while the number is different; the old pointer does not automatically regain validity just because the new object happens to occupy the same address. Caching reduces repeated requests, but the number of retained blocks may diverge from the number of active application blocks.

Preparing the visual
Who Gets Ownership on free?

free ends the application's access to the object, but the block may remain in the allocator. Re-allocation can reuse the address but establishes a new object lifecycle.

mmap Threshold Is Not a Static Boundary

The main arena often expands via the program break, while other arenas can establish mmap-backed regions; independent large mmap allocations serve yet another purpose. Small objects may also reside in mmap-backed arenas.

glibc can dynamically adjust the mmap threshold. Typical adjustments occur when releasing larger mmap chunks that meet conditions, raising the threshold to the corresponding size to avoid repeated requests to the system and un-mapping similar blocks; it does not lower the threshold every time a large block is freed.

Explicitly configuring the corresponding threshold affects dynamic adjustment. The threshold is just one policy; it must be understood in conjunction with existing reusable space and other constraints. See glibc allocation tunables for relevant names and meanings.

Ordinary private anonymous mmap still follows COW (Copy-On-Write) after fork; it is not a copy-once shared mapping. Independent blocks are easily un-mapped individually, which does not mean the system has no fragmentation or page table costs.

Why RSS Might Not Drop After free

After the application releases an object, the allocator may retain the chunk in the cache or arena for reuse by the next request. Even if some bytes are free, if the page they reside in still has active objects, it may not be possible to return the entire page.

For example, if a page has interleaved long-lived small objects and short-lived objects, after all short-lived objects are released, the remaining long-lived objects still keep this page. Simply summing all free bytes cannot deduce the number of returnable pages; this is an external fragmentation problem caused by layout and lifetime.

malloc_trim attempts to release free memory that can be released, possibly by shrinking or releasing page contents. It does not guarantee that all free space in all arenas is returned, nor does it make active objects disappear. Frequent trimming may increase subsequent page fault and reallocation costs. malloc_trim(3)

Releasing 12 KiB forms a whole free page depending on layout Interleaved Lifetimes Page 1 Page 2 Page 3 Page 4 Grouped by Lifetime Page 1 Page 2 Page 3 Page 4 Blue: Still alive; Gray: Freed. Each page represents 4 KiB. The top row has no whole free pages; the bottom row has 3 candidate pages. Whether they can actually be returned is also subject to allocator layout and policy constraints.

Calculate Free Bytes First, Then Returnable Pages

In a model that ignores metadata, each of the 4 pages has 1 KiB of long-lived objects and 3 KiB of short-lived objects. After the short-lived objects are released, the active amount is 4 KiB and the free amount is 12 KiB, but every page still has active data. If objects of the same lifetime are grouped, the 4 KiB of active content occupies only one page, and the other three pages become candidates for whole-page recycling.

page = 4096
interleaved_live = [1024, 1024, 1024, 1024]
grouped_live = [4096, 0, 0, 0]
for live in [interleaved_live, grouped_live]:
    assert sum(live) == 4096
    assert sum(page - n for n in live) == 12288
assert sum(n == 0 for n in interleaved_live) == 0
assert sum(n == 0 for n in grouped_live) == 3

This is not a promise that glibc will strictly place four objects as shown in the diagram, nor does it justify moving arbitrary malloc objects: externally saved pointers prevent ordinary C allocators from freely compacting active objects like some GCs. For a batch of objects with a clear shared lifecycle, application-level region allocation can make releasing the entire batch easier; if a few objects in it have a particularly long lifetime, the entire region may be held by them.

12 KiB Free, Why Can't We Recycle Even One Page?

The number of objects and the number of freed bytes are exactly the same for both layouts; the difference lies only in whether long-lived objects occupy every page. Release in the interleaved layout first, then switch to the grouped layout and redo it. Observe that while the free amount is the same, the number of whole-page candidates changes from 0 to 3. Ordinary malloc cannot arbitrarily move long-lived objects; layout usually needs to start from allocation strategy and object lifetime.

Preparing the visual
12 KiB Free, Why Can't We Recycle Even One Page?

The sum of free bytes does not determine the number of recyclable whole pages; a few long-lived objects scattered across different pages will cause every page to be retained.

Lock Contention, False Sharing, and Leaks Are Different

Multiple threads competing for the same arena mutex is lock contention; two threads modifying different objects on the same cache line generating coherence traffic is typical false sharing. Limiting the number of arenas may reduce reserved memory but increases lock contention; it cannot be treated as a universal option to solve both problems simultaneously.

Leaks usually refer to losing necessary release paths or objects that are no longer needed but continuously retained; the allocator retaining free blocks is a different layer of phenomenon. When judging, compare the amount of active objects, allocator statistics, RSS/PSS, and load phases. See Process Memory Layout.

Observation and Small-Step Tuning

#include <malloc.h>
#include <stdio.h>

int dump_allocator(void) {
    return malloc_info(0, stdout);
}

This is a glibc-specific interface that outputs XML information about arenas, etc. It should not be understood as a complete, precise business survival list of all thread cache objects. malloc_stats and mallinfo2 also have their own statistical scopes and limitations.

GLIBC_TUNABLES=glibc.malloc.arena_max=2 ./prog
GLIBC_TUNABLES=glibc.malloc.tcache_count=0 ./prog

These two commands are used for separate control experiments and do not imply they should be deployed simultaneously to all programs. Record throughput, tail latency, allocation call stacks, and memory curves; avoid evaluating long-term services solely based on RSS at the end of execution.

The MALLOC_TRACE environment variable does not guarantee that the current glibc automatically records all allocations; enabling mtrace and debug library requirements must be configured according to the version. Ordinary out-of-bounds and use-after-free issues should be prioritized for localization via ASan or Memcheck; lowering thresholds cannot be treated as fixing memory errors.

Observation Requires Sampling Along Load Phases

PhaseApplication RecordsAllocator & System Side RecordsUsed to Judge
Pre-warmupInitial active object count and bytesArena info, RSS/PSSBaseline
PeakSize distribution, peak concurrency, lifetimeReserved space and residencyWhat drives the peak
After releasing short-lived objectsHow many released, which remainFree structures and RSS/PSSWhether object release translates to whole-page recycling
Next round of same loadNew request volume, allocation latencyWhether reused, whether growing againWhether cache is stably reused

If business active bytes drop back to baseline, RSS remains high, but the next peak does not grow, the reserved space may be playing a reuse role. If the business active amount continues to rise in the same loop, first find the holders and release paths. If the active amount is stable but system usage continues to rise, then check fragmentation, thread/arena count, mappings, and statistical blind spots; do not announce a leak based solely on one curve.

malloc_info can output information for multiple arenas, but it is not a business object ledger. Sampling itself also has costs; do not format the entire XML after every malloc and then treat the resulting time as the allocator's raw latency. malloc_info(3)

What to Compare When Replacing the Allocator

The current design of Google TCMalloc can use per-CPU caches and has different modes; it and the tcmalloc in gperftools cannot be conflated into the same version just by library name. jemalloc uses mechanisms such as arenas, thread caches, and extents, and its statistics and recycling behaviors are configurable. TCMalloc Design, jemalloc Manual

There is no "large allocation fixed threshold, fragmentation high/low ranking" that holds stable across all loads. You should test object size distribution, lifetime, cross-thread releases, concurrency, and RSS curves. Heap profilers are not equal to CPU profilers; report dimensions need confirmation.

LD_PRELOAD only works for applicable dynamic linking and symbol resolution paths; static linking, custom allocation interfaces, restricted execution environments, and multiple runtimes may change the effect. Allocation and release must be paired by compatible allocator protocols; you cannot switch malloc on one side while letting other components use a mismatched free on the other.

Keywords: malloc, glibc, ptmalloc2, tcache, arena, bins, fragmentation, mmap threshold, RSS, malloc_trim, jemalloc, TCMalloc