内存管理:栈与堆

注:

  • 栈和堆中的栈

    指内存管理中的栈,本质是内存区域,是计算机系统的底层机制,用于支持函数调用和局部存储,由硬件直接管理

  • 栈和队列中的栈

    指数据结构中的栈,本质是抽象数据类型。是高级编程中的抽象工具,用于算法实现,需要手动管理

两种栈虽然都遵循后进先出(LIFO)原则,但是两种不同的概念。

本文提及的栈为内存管理中的栈。

一、定义

1.1 栈(Stack)

栈是一种由系统自动管理的连续内存区域,其分配和释放遵循严格的"后进先出"(LIFO)原则。在程序运行时,栈主要用于:

  1. 存储函数调用上下文(包括返回地址、参数和局部变量)
  2. 维护线程执行状态
  3. 通过CPU寄存器(如x86的ESP/RSP)直接管理内存分配

核心特性:

  • 分配/释放仅需移动栈指针(O(1)时间复杂度)

[!NOTE]

栈指针

栈指针(Stack Pointer,简称SP)是CPU中的一个专用寄存器,用于实时跟踪栈顶内存地址。它相当于一个"书签",始终标记着栈内存中当前可用的最顶部位置。

  • 内存生命周期函数作用域严格绑定
  • 具有固定大小限制(通常1-8MB)
  • 访问速度极快(直接CPU寄存器操作)

使用示例

void function() {
   int a = 10;
   char b = 'x';
} // 函数结束 生命周期结束

1.2 堆(Heap)

堆是程序运行时可动态分配的内存池,其特点包括:

  1. 需要显式请求分配(malloc/new)和释放(free/delete)
  2. 内存块可以按任意顺序分配和释放
  3. 支持动态调整大小(realloc)
  4. 通过指针间接访问分配的内存区域

核心特性:

  • 分配算法复杂度取决于实现(通常O(1)~O(n))
  • 内存生命周期持续到显式释放或程序终止
  • 容量仅受系统可用内存限制
  • 可能产生内存碎片(外部碎片问题)

[!NOTE]

内存碎片

申请第1、2、3块内存,如图

在这里插入图片描述

释放第2块内存时,无需先释放第3块内存,如图

在这里插入图片描述

申请第4、5块内存,如图

在这里插入图片描述

以上过程直观地展示了内存碎片的产生。

使用示例

void buildCity() {
    int* building = malloc(100 * sizeof(int));
    free(building); //需要释放
}

二、特点

2.1 栈的特点

  • 自动生命周期:函数调用时创建,返回时销毁
  • 固定大小:Linux默认8MB(ulimit -s查看)
  • 极速访问:只需移动栈指针(SS:ESP寄存器)
  • 严格有序:后进先出的严格顺序
  • 线程私有:每个线程有自己的栈空间

2.2 堆的特点

  • 动态分配:运行时决定内存大小
  • 全局可见:分配的内存可跨函数使用
  • 手动管理:必须显式释放(C语言)
  • 碎片风险:频繁分配释放会产生"内存空洞"
  • 大容量:理论可达TB级(受系统限制)

三、时间复杂度分析

3.1 栈操作复杂度

操作 时间复杂度 底层实现
内存分配 O(1) 修改ESP寄存器
内存释放 O(1) 恢复ESP寄存器
访问变量 O(1) 基址+偏移量寻址

3.2 堆操作复杂度

操作 时间复杂度 常见实现方式
malloc() O(n) 遍历空闲链表
free() O(1)~O(n) 合并相邻空闲块
realloc() O(n) 可能触发内存拷贝

(堆的时间复杂度计算见后文实现机制)

四、实现机制

4.1 堆的管理算法

1. malloc()

时间复杂度取决于 内存分配算法当前堆的状态

(1)首次适应(First-Fit)算法
  • 最坏情况O(n)
    需要遍历空闲链表的所有块,直到找到第一个足够大的块

    // 空闲链表结构示例
    struct free_block {
        size_t size;
        struct free_block* next;
    };
    
  • 平均情况O(n/2)
    假设空闲块随机分布,平均需要检查一半的块

(2)最佳适应(Best-Fit)算法
  • 固定为 O(n)
    必须遍历所有空闲块才能确定"最小合适"的块
(3)伙伴系统(Buddy System)
  • 稳定 O(log n)
    通过二叉树分割合并,每次操作只需遍历树高

    初始16KB内存:
    申请3KB → 分割为8KB→4KB→分配4KB(3次操作)

2. free()

(1)基础实现
  • 首次适应/最佳适应O(n)
    需要合并相邻空闲块,可能遍历整个空闲链表
(2)伙伴系统
  • 稳定 O(log n)
    递归合并"伙伴块"直到无法继续合并

3. realloc()

(1)原地扩容(最佳情况)
  • O(1)
    当后续内存块空闲且足够大时:

    void* realloc(void* ptr, size_t new_size) {
        if (next_block_is_free && combined_size >= new_size) {
            merge_blocks(); // 直接合并相邻块
            return ptr;     // 原指针不变
        }
    }
    
(2)需迁移数据(最坏情况)
  • O(n)
    当无法原地扩展时:
    1. malloc(new_size)O(n)
    2. memcpy(新地址, 旧地址, 旧大小)O(n)
    3. free(旧地址)O(n)
      总复杂度O(3n) ≈ O(n)

五、应用场景

5.1 栈的理想场景

  1. 函数调用:保存返回地址/参数

    int add(int a, int b) { // a,b在栈上
        return a + b;
    }
    
  2. 局部变量:生命周期明确的小数据

    void process() {
        char buffer[1024]; // 适合栈分配
    }
    
  3. 中断处理:需要快速响应的场景

5.2 堆的必要场景

  1. 动态数据结构

    typedef struct Node {
        int data;
        struct Node* next; // 必须用堆
    } Node;
    
  2. 大内存需求

    void* bigData = malloc(10 * 1024 * 1024); // 10MB
    
  3. 跨函数共享数据

    int* createArray(int size) {
        return malloc(size * sizeof(int));
    }
    

六、注意事项

6.1 栈的注意事项

  • 栈溢出攻击:缓冲区溢出可能覆盖返回地址

    void vulnerable() {
        char buf[8];
        gets(buf); // 可能溢出
    }
    
  • 返回栈指针

    int* danger() {
        int x = 10;
        return &x; // 错误 因为x将失效
    }
    

6.2 堆的注意事项

  • 悬垂指针

    int* ptr = malloc(sizeof(int));
    free(ptr);
    *ptr = 10; // ptr已经被释放
    

七、对比

维度 栈(Stack) 堆(Heap)
管理方式 编译器自动管理 程序员手动管理(C)/GC(Java)
分配速度 纳秒级(寄存器操作) 微秒级(需系统调用)
容量限制 MB级(可调但有限) GB/TB级(受OS限制)
生命周期 函数作用域 直到显式释放
碎片问题 可能严重
访问方式 直接CPU寻址 通过指针间接访问
线程安全 线程私有 全局共享

八、C与Java的跨语言对比

8.1 内存模型差异

// Java示例
public class MemoryModel {
    public static void main(String[] args) {
        int stackVar = 42;                // 栈存储
        Object heapObj = new Object();    // 堆存储
        String poolStr = "常量池";        // 方法区存储
    }
}

8.2 关键区别对比

特性 C语言 Java
栈存储 所有局部变量 基本类型+对象引用
堆管理 完全手动 GC自动管理(分代收集)
内存泄漏 必须显式free 对象不可达即视为泄漏
指针安全 裸指针易出错 引用类型安全
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐