堆与栈

1 堆与栈

1.使用内存时,需要考虑分配、回收2个问题。
2.堆和栈是使用内存的2种方式。
3.堆通用性好,但需要复杂的内存管理算法配合使用;栈针对函数做了优化,性能很好,但通用性差。

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

  1. 把内存的【分配、回收】和应用程序实际内存读写分开
  2. 【内存管理模块】负责内存的【分配、回收】,并为应用程序提供接口
  3. 应用程序通过接口完成内存的【分配、释放】

比如下面的Allocator就是专门负责内存管理的模块,它对外提供2个接口:alloc()和free()
应用程序需要新内存时,通过调用alloc()分配;用完之后,要再调用free()释放(回收)

public class Allocator {

    // 预先把内存分成固定大小的块
    private List<Pointer> freeList = new LinkedList<>();

    // 分配内存时,直接拿一个空闲块返回
    public Pointer alloc() {
        Pointer ret = freeList.removeFirst();
        return ret;
    }
    
    // 释放(回收)内存时,把要回收的块加入空闲链表
    public void free(Pointer p) {
        freeList.add(p);
    }
}

这是一个非常粗糙的例子,实际场景中应用程序会要求分配不同大小的块,比如: 12字节,48字节等
我们可以把内存分成不同大小的块,每种大小分别对应自己的freeList
然后选择比程序要求的大,但又最接近的块返回,以减少内存的浪费
实际情况会更复杂,这里只是让读者对堆有一个大致的概念
PS: Java语言不需要程序员自己调用free(),而是为堆引入了自动回收机制(GC)

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

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

这个优化思路会衍生出2个问题:

问题 说明 解决方案
函数本身内存占用不能太大 否则,等不到函数结束,内存就已经耗尽了 让堆帮忙分摊压力函数执行过程中,涉及的基本类型变量和引用(指针)放在栈上复杂对象,占用内存较大,还是放在堆上
函数执行前的状态不能太复杂 否则,记录和恢复的开销都太大,优化就没有意义了 限制栈的操作:只能在栈顶处进行操作(push/pop)通过这个限制,只需要“栈顶位置”这一个参数就可以描述函数执行前的状态了

下面通过一个实例,具体说明一下:

public void f() {
    int avg = avg(5, 7);
    //......
}

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

汇编指令参考:intel developer manual download

阶段 对应汇编指令 说明
阶段1准备参数调用avg() ...... - f()调用avg()之前栈上的内容
7 push 7 把参数7放到栈上
5 push 5 把参数5放到栈上
<下一条指令的地址> call <函数avg()的地址> call指令用于函数调用它会自动把函数返回地址放到栈上
阶段2执行avg()准备返回值 (函数f的)ebp push ebp 此时,ebp对应的是函数f()的栈帧的起始地址把函数f()的栈帧起始位置暂存到栈上,腾出寄存器ebpPS: ebp = extended base pointer
mov ebp, esp 把esp指向的栈顶地址暂存到ebp中因为接下来就要执行avg()了,所以这里记录下执行前的位置,方便后续恢复到执行前的状态为函数avg()生成新的栈帧栈帧的起始地址暂存在ebp中PS: esp = extended stack pointer
...... 函数avg()内部指令...... -
...... 函数avg()内部指令...... -
...... 函数avg()内部指令...... -
mov eax, 通过eax寄存器返回结果
阶段3回收avg()占用的内存 img mov esp, ebp 和前面的 move ebp, esp相对,让esp指向ebp中保存的地址 ebp里保存的就是avg()执行前的位置达成的效果是:移除avg()的栈帧,回收avg()占用的内存 即:把栈恢复到执行avg()前的状态
阶段4恢复函数f()的栈帧 img pop ebp 弹出栈顶元素,放入寄存器ebp和之前的push ebp相对,恢复函数f()的栈帧
阶段5返回函数f(), 继续执行 img ret ret指令会弹出栈顶元素作为指令返回地址CPU会从返回地址处,继续执行 这里参数7和5还在栈上,函数f()可以根据自己的需要决定是否弹出这两个参数
; 为调用avg()做准备
; 传参
push 7
; 传参
push 5
; 实际调用avg()
call <address of avg()>

; ----------------- avg() start --------------------
; 保存上一个函数的栈帧
push ebp

; 为函数avg()创建栈帧
mov ebp, esp

; avg函数内部指令
; ......
; ......

; 通过eax传递返回值
mov eax, <result>

; 移除avg()的栈帧  -- 回收函数avg()占用的内存
mov esp, ebp
; 恢复f()的栈帧
pop ebp
; avg()返回, 继续执行f()
ret
; --------------- avg() end -------------------------

; 参数7和5还在栈上,函数f()可以根据自己的需要决定是否把它们从栈上移除

补充说明

  1. 栈的高效来源于对栈使用方式的限制 如果不是限制了栈的使用方式,函数的起始状态就不会这么简单,进而栈内存的回收就不会这么高效
  2. 每个线程都有自己独立的栈,栈上记录着线程的“执行轨迹” debug时看的线程堆栈,就是从栈上获取的信息 PS:我们后面讲Java的GC时,会涉及GC roots的概念,这一点对理解GC roots至关重要
  3. 堆是多个线程共享的,Java的堆通过GC算法进行自动回收
    原链接:https://v0etqjz8nkv.feishu.cn/docx/Esh0dPqbYoDgCgxaBhdcM3MOnyf
posted @ 2024-03-07 20:14  JaxYoun  阅读(2)  评论(0编辑  收藏  举报