栈与堆的区别

Thu Sep 12 2024

理解栈与堆

如何理解

栈和堆其实是我们的两种使用内存的2种不同方式,内存使用涉及两个基本问题:内存分配与内存回收 想要理解栈与堆的区别,就需要从内存分配和回收的角度去分析思路 总结下来就是

  1. 使用内存时,需要考虑内存分配和回收的问题
  2. 栈和堆就是使用内存的两种不同方式
  3. 堆的通用性好,但是需要配合复杂的内存管理使用
  4. 栈针对于函数做了优化,性能好,但通用性差

堆的设计核心思路是:关注点分离

  1. 把内存的分配回收和应用的实际读写内存分开
  2. 内存的管理模块去控制内存的分配和回收,并为应用程序提供接口
  3. 应用程序通过接口去完成内存的分配和释放
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Allocator {
    // 预先把需要的内存分成固定大小的块
    private List<Pointer> freeList = new LinkedList<>();

    // 分配内存时,拿出一个空闲的块返回
    public Pointer alloc() {
        Pointer ret = freeList.removeFirst();
        return 0;
    }

    // 释放内存时,把要回收的内存块加入空闲链表
    public void free(Pointer p) {
        freeList.add(p);
    }
}

但是在实际应用中,应用程序会要求分配不同大小块,可能是24字节,8字节等等 这个时候我们就可以把内存分配成不同大小的块,每种大小都对应这自己的freeList 然后选择比应用程序要求大,但又最接近的块返回,以减少内存的浪费

栈是针对与函数调用做的优化,它的设计核心思路: 记录 - 恢复

  1. 函数开始执行前,记录内存的使用情况
  2. 函数结束执行后,直接恢复到开始的记录的状态(这就相当于回收了当前函数分配的内存)

但这样的优化思路会出现两个问题

  1. 函数本身占用的内存不是很大,即等不到函数结束时,内存就已经耗尽了 解决方方法:让堆分摊内存压力 涉及基本类型变量和引用(指针)可以放在栈上 消耗内存大的放在堆上
  2. 函数执行前的环境不能太复杂,如果环境太复杂那栈执行的记录和和恢复花销就太大了,优化就没有太大的意义 解决方方法:限制栈的操作,只能在栈顶处进行操作(push/pop) 通过这个限制就只需要记录“栈顶位置”就可以描述函数执行前的状态
JAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void f() {
  //f()调用avg之前栈上的内存
  int avg = avg(5,7)
  /* push 5 把参数5放在栈上
     push 7 把参数7放在栈上*/
  // ....
  /*call指令用于函数调用
    它会自动把函数返回的地址放到栈上*/
}

public int avg(int x, int y) {
  int sum = x + y;
  return sum / 2;
}

在执行次代码会有三个阶段

  1. 阶段一

    1.准备参数 2.调用avg

  2. 阶段二

    1.执行avg() 2.准备返回值

    进入avg函数内部,会先保存avg正式开始执行前的栈的状态,有两条指令

    • push ebp 此时ebp里面记录的是函数f()的栈帧起始地址 现在的操作是把ebp的栈帧起始地址暂存到栈上,从而腾出ebp push就是压栈

    • mov ebp,esp 把esp指向的栈顶地址,暂存到ebp里 PS:esp这个寄存器总是指向栈顶

  3. 阶段三

    1.回收avg()占用的内存

    • mov esp ebp 这个指令是与mov ebp,esp相对的。

      达成的效果是:移除avg()的栈帧,回收avg()占用的内存 即:把栈恢复到执行avg()之前的状态

  4. 阶段四

    1.恢复函数f()的栈帧

    • pop ebp 即:恢复函数f()的栈帧,与push ebp相对 pop就是出栈
  5. 阶段五

    1.返回函数f(),继续执行代码

    • ret ret指令会弹出栈顶元素作为指令的返回地址,CPU会从返回地址,继续执行代码

    注:此处参数5和7还在栈上,函数f()可以根据自己的需要决定是否弹出这两个参数