From Transistor to Browser

The Software · Chapter 9.b
Where does new memory come from?

Updated Oct 12, 2026
Summarize
Transcript

Where does new memory come from? A program keeps asking for memory, in sizes it can't predict.

I figured you'd keep a marker and just slide it forward for each request. But blocks come back in any order, and they leave holes.

So an allocator keeps a list of free runs. Sixteen cells, all free. Ask for A, B and C: each takes four cells from the front. Free A: a hole opens at the start. Free B: now two holes touch. The first hole ends where the second starts, so they merge into one run of eight. Without that merge, a request for eight would fail, with the room right there. Free C, and the heap is one run again.

So new memory comes from holes, merged back together! No space lost and no gaps left behind: that's the allocator's whole job.

Carve blocks out below and free them in any order. Can you leave a hole that won't merge?

The hosts' voices are synthetic, made with Breeze TTS 2 by BreezeBlue.

  1. The browser
  2. The network
  3. Software and languages
  4. The CPU
  5. Memory
  6. Arithmetic
  7. Logic gates
  8. The switch
Figure 9.b.1: Where we are in the climb from transistor to browser: on the software rung. This chapter asks where new memory comes from, and how the heap hands it out and takes it back.

A program rarely knows in advance how much memory it will need. It asks for some, uses it, gives it back, and asks again, in sizes and at moments it cannot predict. These requests usually go first to an allocator inside the process, such as the library routines malloc and free. The allocator subdivides memory already obtained from the operating system. Kernel requests like those of Chapter 9.a acquire or release larger regions as needed; each small allocation or release need not make a system call.

The region managed this way is the heap, and the allocator is its bookkeeper. Kernels can use similar bookkeeping internally, but this chapter’s Heap is an independent model of one fixed region, with no kernel calls. It tracks address ranges rather than storing the bytes in them.

The whole problem is the unpredictable order. If memory were only ever taken and never returned, handing it out would be trivial: keep a mark and move it forward. But blocks come back in any order, leaving holes, and the allocator must reuse those holes or the heap fills with gaps and can satisfy nothing.

9.b.1 The free list

The allocator keeps a free list: the runs of memory not currently in use, each a start and a length. To satisfy a request it finds a run large enough and carves the request off the front, leaving the remainder free. Figure 9.b.2 follows a small heap through a few requests.

heap-stripheap-strip
Figure 9.b.2: A heap of sixteen cells over time. Three blocks are carved off the front (a, b, c); freeing a leaves a hole; freeing b, whose run touches the hole, merges them into one larger free run.

The interactive version needs the in-browser runtime.

Example 9.b-1. Handing out and reclaiming memory

class Heap:
    """A region of memory, tracked as a free list of (start, length) runs and a
    map of the live allocations."""

    def __init__(self, size: int) -> None:
        self.size = size
        self.free = [(0, size)]   # runs not in use, kept sorted by start
        self.live = {}            # start address -> length of a live block

    def malloc(self, amount: int) -> int:
        """Carve `amount` cells off the first free run large enough and return the
        start address. Raise if no run is big enough."""
        if amount <= 0:
            raise ValueError("allocation size must be positive")
        for index, (start, length) in enumerate(self.free):
            if length >= amount:1
                if length == amount:
                    del self.free[index]
                else:
                    self.free[index] = (start + amount, length - amount)
                self.live[start] = amount
                return start
        raise MemoryError(f"no free run of {amount} cells")

    def release(self, address: int) -> None:
        """Return a live block to the free list, then merge it with any adjacent
        free run so the freed space does not stay fragmented."""
        amount = self.live.pop(address)
        self.free.append((address, amount))2
        self.free.sort()
        self._coalesce()

    def _coalesce(self) -> None:
        """Merge free runs that touch into single runs."""
        merged = []
        for start, length in self.free:
            if merged and merged[-1][0] + merged[-1][1] == start:3
                end = merged[-1][0]
                merged[-1] = (end, merged[-1][1] + length)
            else:
                merged.append((start, length))
        self.free = merged

Taking the first run large enough is the placement policy called first fit. It is the simplest choice, not the only one; real allocators use others, such as best fit, buddy systems, and size classes, to trade speed against how much the heap fragments.

9.b.2 Coalescing

Coalescing is what keeps the heap healthy. Without it, freeing a block next to an existing hole would leave two small runs where there should be one large one, and a later request for the combined size would fail even though the room exists. Merging adjacent runs, shown in Figure 9.b.3, undoes the fragmentation that freeing creates.

coalescecoalesce
Figure 9.b.3: Coalescing two free runs. Because the first run’s start plus its length equals the second’s start, the runs are adjacent, so they merge into one. This is the test on the free list each time a block is freed.

9.b.3 The allocator, run

Running ftb.allocator allocates three blocks, frees them so that two freed runs sit next to each other, and shows the free list at each step.

output · ftb.allocator
a heap of 16 cells, all free: [(0, 16)]

malloc(4) three times: a at 0, b at 4, c at 8
  free list now: [(12, 4)]

free a: leaves a hole at 0
  free list now: [(0, 4), (12, 4)]
free b: its run touches the hole, so they coalesce
  free list now: [(0, 8), (12, 4)]

free c: everything is free again, one run: [(0, 16)]

The three allocations carve the heap from the front. Freeing a opens a hole at the start; freeing b adds a run that touches it, and the two coalesce into a run of eight. When the last block goes back, the heap is a single free run again, exactly as it started. That round trip, no space lost and no gaps left behind, is the allocator’s whole job.

The allocator decides which address ranges are free. The next chapter asks how a program’s addresses map to physical memory, and what keeps private regions apart.

Exercises