堆与栈
1 堆与栈
1.使用内存时,需要考虑分配、回收2个问题。
2.堆和栈是使用内存的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)
栈
栈是针对函数调用做的优化,它的设计思路是:记录 - 恢复
- 函数开始执行前,记录内存的使用情况
- 函数结束后,直接恢复到开始前的状态 这样就回收了当前函数新分配的内存
这个优化思路会衍生出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()占用的内存 | mov esp, ebp | 和前面的 move ebp, esp相对,让esp指向ebp中保存的地址 ebp里保存的就是avg()执行前的位置达成的效果是:移除avg()的栈帧,回收avg()占用的内存 即:把栈恢复到执行avg()前的状态 | |
阶段4恢复函数f()的栈帧 | pop ebp | 弹出栈顶元素,放入寄存器ebp和之前的push ebp相对,恢复函数f()的栈帧 | |
阶段5返回函数f(), 继续执行 | 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()可以根据自己的需要决定是否把它们从栈上移除
补充说明
- 栈的高效来源于对栈使用方式的限制 如果不是限制了栈的使用方式,函数的起始状态就不会这么简单,进而栈内存的回收就不会这么高效
- 每个线程都有自己独立的栈,栈上记录着线程的“执行轨迹” debug时看的线程堆栈,就是从栈上获取的信息 PS:我们后面讲Java的GC时,会涉及GC roots的概念,这一点对理解GC roots至关重要
- 堆是多个线程共享的,Java的堆通过GC算法进行自动回收
原链接:https://v0etqjz8nkv.feishu.cn/docx/Esh0dPqbYoDgCgxaBhdcM3MOnyf
学习使我充实,分享给我快乐!