Memory Allocation
[!NOTE] This module explores the core principles of Memory Allocation, deriving solutions from first principles and hardware constraints to build world-class, production-ready expertise.
1. The Problem: The Heap is Manual
When you declare int x;, the compiler allocates it on the Stack. It’s fast and automatic.
When you call malloc(100), you are asking for 100 bytes on the Heap. The OS must find a free hole in the chaos.
The Memory Allocator (part of libc or the runtime) manages a large chunk of memory requested from the kernel (via sbrk or mmap).
Fragmentation: The Enemy
- External Fragmentation: Total free memory is enough, but it’s scattered in small non-contiguous holes.
- Internal Fragmentation: You ask for 10 bytes, but the allocator gives you a 32-byte block (padding). The extra 22 bytes are wasted.
2. Allocation Strategies
1. Free List (First Fit / Best Fit)
Maintain a Linked List of free blocks.
- First Fit: Scan the list, take the first hole that fits. Fast, but fragments memory.
- Best Fit: Scan the whole list, find the smallest hole that fits. Slow (O(N)), but saves large holes.
2. The Buddy System
Divide memory into powers of 2 (4KB, 8KB, 16KB…). If you need 7KB:
- Split a 16KB block into two 8KB “buddies”.
- Give one 8KB block to the user. Coalescing: When you free the 8KB block, check if its “buddy” is also free. If so, merge them back into a 16KB block.
3. Slab Allocation (Kernel Favorite)
The OS Kernel creates frequent objects of the same size (e.g., task_struct, inode).
Instead of a generic allocator, we create a Cache (Slab) for each object type.
- Pros: Zero fragmentation (objects fit perfectly). Extremely fast allocation (pop from free list).
3. Interactive: Allocator Visualizer
Simulate a simple Heap Allocator. Watch how random allocations create External Fragmentation (holes).
4. Code Example: Low-Level Allocation
How do languages interact with raw memory?
- Go: Uses
unsafe.Pointerto bypass type safety. - Java: Uses
ByteBuffer(orUnsafe) to allocate off-heap memory (outside GC control).
package main
import (
"fmt"
"unsafe"
)
type Header struct {
Size int
Free bool
}
func main() {
// Simulate a raw memory block
// [Header][Data.....][Header][Data.....]
// 1. Create a byte array acting as our "Heap"
heap := make([]byte, 1024)
// 2. Cast the beginning to a Header struct
ptr := unsafe.Pointer(&heap[0])
header := (*Header)(ptr)
header.Size = 64
header.Free = false
fmt.Printf("Allocated Block at %p\n", header)
fmt.Printf("Block Size: %d\n", header.Size)
// 3. Calculate address of next block
// Pointer Arithmetic: ptr + sizeof(Header) + Size
nextPtr := uintptr(ptr) + unsafe.Sizeof(*header) + uintptr(header.Size)
nextHeader := (*Header)(unsafe.Pointer(nextPtr))
nextHeader.Size = 128
nextHeader.Free = true
fmt.Printf("Next Free Block at 0x%x\n", nextPtr)
}
import java.nio.ByteBuffer;
public class OffHeap {
public static void main(String[] args) {
// Allocate 1KB Off-Heap (Directly in OS RAM, not JVM Heap)
// This is not subject to GC! You must manage it manually.
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// Write data "manually" like C
// Address 0: 42
buffer.putInt(0, 42);
// Address 4: 123
buffer.putInt(4, 123);
System.out.println("Value at offset 0: " + buffer.getInt(0));
System.out.println("Value at offset 4: " + buffer.getInt(4));
// If we lose the reference to 'buffer', the Cleaner will eventually
// free the OS memory, but it's non-deterministic.
}
}