Skip to content

Zig Memory Management

Memory management is a core concept in systems programming. Zig provides powerful and flexible memory management mechanisms that allow developers to precisely control memory allocation and deallocation.

Memory Management Basics

Stack Memory vs Heap Memory

zig
const std = @import("std");

pub fn main() void {
    // Stack memory: automatically managed, freed when function ends
    var stack_array: [100]i32 = undefined;
    const stack_value = 42;
    
    std.debug.print("Stack variable address: {*}\n", .{&stack_value});
    std.debug.print("Stack array address: {*}\n", .{&stack_array});
    
    // Stack memory lifetime is determined by scope
    {
        var local_var = 123;
        std.debug.print("Local variable address: {*}\n", .{&local_var});
    } // local_var is destroyed here
}

Allocator Concept

Zig uses allocators to manage heap memory:

zig
const std = @import("std");

pub fn main() !void {
    // Create general purpose allocator
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    // Allocate memory
    const memory = try allocator.alloc(u8, 100);
    defer allocator.free(memory); // Important: free memory
    
    std.debug.print("Allocated {} bytes of memory\n", .{memory.len});
    std.debug.print("Memory address: {*}\n", .{memory.ptr});
    
    // Use memory
    for (memory, 0..) |*byte, i| {
        byte.* = @intCast(i % 256);
    }
    
    std.debug.print("First 10 bytes: ");
    for (memory[0..10]) |byte| {
        std.debug.print("{} ", .{byte});
    }
    std.debug.print("\n");
}

Allocator Types

General Purpose Allocator

zig
const std = @import("std");

pub fn main() !void {
    // General purpose allocator: suitable for most uses, has memory leak detection
    var gpa = std.heap.GeneralPurposeAllocator(.{
        .safety = true,  // Enable safety checks
        .thread_safe = true,  // Thread safe
    }){};
    defer {
        const leaked = gpa.deinit();
        if (leaked == .leak) {
            std.debug.print("Memory leak detected!\n", .{});
        }
    }
    
    const allocator = gpa.allocator();
    
    // Allocate different sizes of memory
    const small_mem = try allocator.alloc(u8, 10);
    const large_mem = try allocator.alloc(i32, 1000);
    
    defer allocator.free(small_mem);
    defer allocator.free(large_mem);
    
    std.debug.print("Small memory: {} bytes\n", .{small_mem.len});
    std.debug.print("Large memory: {} integers\n", .{large_mem.len});
}

Page Allocator

zig
const std = @import("std");

pub fn main() !void {
    // Page allocator: allocates pages directly from OS
    const allocator = std.heap.page_allocator;
    
    // Allocate large blocks of memory
    const big_memory = try allocator.alloc(u8, 4096 * 10); // 10 pages
    defer allocator.free(big_memory);
    
    std.debug.print("Allocated {} bytes ({} pages)\n", .{ big_memory.len, big_memory.len / 4096 });
    
    // Page allocator is suitable for large memory allocations
    const huge_array = try allocator.alloc(f64, 100000);
    defer allocator.free(huge_array);
    
    std.debug.print("Allocated {} floats\n", .{huge_array.len});
}

Fixed Buffer Allocator

zig
const std = @import("std");

pub fn main() !void {
    // Pre-allocated buffer
    var buffer: [1024]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buffer);
    const allocator = fba.allocator();
    
    // Allocate from fixed buffer
    const mem1 = try allocator.alloc(u8, 100);
    const mem2 = try allocator.alloc(i32, 50);
    
    std.debug.print("Allocated from fixed buffer:\n");
    std.debug.print("  Memory 1: {} bytes\n", .{mem1.len});
    std.debug.print("  Memory 2: {} integers\n", .{mem2.len});
    std.debug.print("  Remaining space: {} bytes\n", .{fba.end_index});
    
    // Fixed buffer allocator doesn't need explicit freeing
    // All memory is freed when fba goes out of scope
}

Arena Allocator

zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    
    // Create arena allocator
    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit(); // Free all memory at once
    
    const allocator = arena.allocator();
    
    // Allocate multiple memory blocks
    var allocations = std.ArrayList([]u8).init(gpa.allocator());
    defer allocations.deinit();
    
    for (0..10) |i| {
        const size = (i + 1) * 10;
        const memory = try allocator.alloc(u8, size);
        try allocations.append(memory);
        
        std.debug.print("Allocation {}: {} bytes\n", .{ i, size });
    }
    
    std.debug.print("Total allocations: {}\n", .{allocations.items.len});
    
    // arena.deinit() frees all allocated memory
    // No need to free each memory block individually
}

Memory Allocation and Deallocation

Basic Allocation Operations

zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    // alloc: allocate uninitialized memory
    const uninit_memory = try allocator.alloc(i32, 5);
    defer allocator.free(uninit_memory);
    
    // Initialize memory
    for (uninit_memory, 0..) |*item, i| {
        item.* = @intCast(i * 10);
    }
    
    std.debug.print("Uninitialized memory: ");
    for (uninit_memory) |item| {
        std.debug.print("{} ", .{item});
    }
    std.debug.print("\n");
    
    // allocSentinel: allocate memory with sentinel value
    const sentinel_memory = try allocator.allocSentinel(u8, 10, 0);
    defer allocator.free(sentinel_memory);
    
    // Copy string to sentinel memory
    @memcpy(sentinel_memory[0..5], "Hello");
    std.debug.print("Sentinel memory: {s}\n", .{sentinel_memory});
    
    // dupe: duplicate existing data
    const original = [_]i32{ 1, 2, 3, 4, 5 };
    const duplicated = try allocator.dupe(i32, &original);
    defer allocator.free(duplicated);
    
    std.debug.print("Duplicated data: ");
    for (duplicated) |item| {
        std.debug.print("{} ", .{item});
    }
    std.debug.print("\n");
}

Memory Safety

Memory Leak Detection

zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{
        .safety = true,
    }){};
    
    const allocator = gpa.allocator();
    
    // Normal memory usage
    const good_memory = try allocator.alloc(u8, 100);
    allocator.free(good_memory);
    
    // Intentionally create memory leak (for demonstration only)
    const leaked_memory = try allocator.alloc(u8, 50);
    _ = leaked_memory; // Intentionally not freed
    
    // Check for memory leaks
    const leaked = gpa.deinit();
    switch (leaked) {
        .ok => std.debug.print("No memory leaks\n", .{}),
        .leak => std.debug.print("Memory leak detected!\n", .{}),
    }
}

Memory Management Best Practices

1. RAII Pattern

zig
const std = @import("std");

const ManagedBuffer = struct {
    data: []u8,
    allocator: std.mem.Allocator,
    
    const Self = @This();
    
    pub fn init(allocator: std.mem.Allocator, size: usize) !Self {
        const data = try allocator.alloc(u8, size);
        return Self{
            .data = data,
            .allocator = allocator,
        };
    }
    
    pub fn deinit(self: *Self) void {
        self.allocator.free(self.data);
    }
    
    pub fn fill(self: *Self, value: u8) void {
        @memset(self.data, value);
    }
    
    pub fn getData(self: *const Self) []const u8 {
        return self.data;
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    // Use RAII pattern to manage memory
    var buffer = try ManagedBuffer.init(allocator, 100);
    defer buffer.deinit(); // Automatic cleanup
    
    buffer.fill(42);
    
    std.debug.print("Buffer size: {}\n", .{buffer.getData().len});
    std.debug.print("First 5 bytes: ");
    for (buffer.getData()[0..5]) |byte| {
        std.debug.print("{} ", .{byte});
    }
    std.debug.print("\n");
}

2. Use defer to Ensure Cleanup

zig
const std = @import("std");

fn processData(allocator: std.mem.Allocator) !void {
    // Allocate multiple resources
    const buffer1 = try allocator.alloc(u8, 1000);
    defer allocator.free(buffer1);
    
    const buffer2 = try allocator.alloc(i32, 500);
    defer allocator.free(buffer2);
    
    const buffer3 = try allocator.alloc(f64, 200);
    defer allocator.free(buffer3);
    
    // Process data...
    std.debug.print("Processing {} + {} + {} bytes of data\n", 
                    .{ buffer1.len, buffer2.len * @sizeOf(i32), buffer3.len * @sizeOf(f64) });
    
    // All defer statements execute automatically at function end
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    try processData(allocator);
    
    std.debug.print("Data processing complete, memory cleaned up\n", .{});
}

Summary

This chapter covered Zig's memory management system in detail:

  • ✅ Difference between stack and heap memory
  • ✅ Various allocator types and their use cases
  • ✅ Memory allocation, deallocation, and resizing
  • ✅ Memory alignment and safety checks
  • ✅ Custom allocator implementation
  • ✅ Memory pool and object pool patterns
  • ✅ Memory management best practices

Zig's memory management system provides great flexibility and control, allowing developers to choose the most appropriate memory management strategy based on specific needs. In the next chapter, we'll learn about Zig's compile-time features.

Content is for learning and research only.