堆上!按需增长 vs OS 线程的固定 1MB——挂起是拆卸调用链,恢复是重组
函数调用时,栈用来存放局部变量和返回地址:
void a() {
int x = 1; // ← 局部变量 x → 压栈
b(); // ← 返回地址 → 压栈
} // ← a 结束,x 和返回地址弹栈
void b() {
int y = 2; // ← y 压栈
c();
}
void c() {
int z = 3; // ← z 压栈
}
三个问题:
协程的局部变量是存在堆上的状态机对象的字段里。调用链的每一层,都是一个独立对象。
suspend fun a() {
val x = 1 // 字段在 a 的 Continuation 对象里
b() // 调用 suspend b
}
suspend fun b() {
val y = 2 // 字段在 b 的 Continuation 对象里
c()
}
suspend fun c() {
val z = 3 // 字段在 c 的 Continuation 对象里
}
线程跟协程的调用链完全解绑了。线程去跑 D,A→B→C 的完整上下文全在堆上睡着。
挂起不是"停在某行代码上",而是整条调用链一路 return,把状态留在堆上的续体对象里。
恢复不需要重建栈——东西一直在堆上,只是没有线程理它。就像一个暂停的游戏存档,读档时所有状态都在,直接从上次停的地方继续。
1000 × 1MB 栈 = 1GB 栈内存
1000 个线程 = 不可能(现实最多几千)
每个按需生长,总共可能就几百 KB
1000 个协程 = 轻松
| OS 线程 | 协程 | |
|---|---|---|
| 递归 10000 层 | 1MB 栈 → 不够 → StackOverflow | 堆 → 10000 个续体对象 → 不够? |
| 存储位置 | 虚拟地址空间的栈区(固定上限) | 堆(可以用到好几个 GB) |
| 失败模式 | StackOverflowError | OutOfMemoryError |
| 实际业务 | 递归几百层就可能崩 | 实际业务中很少用到 10000 层调用 |
简单说:协程的调用链是堆上的链表,不是连续的内存块。堆可以用到好几个 GB,而 OS 线程栈只有 1MB。所以协程不会有 StackOverflow——只会 OutOfMemory,但在实际业务中几乎不可能到那个量级。
OS 线程栈 = 一块连续、固定 1MB 的内存,在虚拟地址空间里,预先分配,不能伸缩。协程的"栈" = 堆上的续体对象链表,每个调用层一个对象,用多少占多少,随时挂起随时恢复。挂起 = 线程离开这个链表,但链表完整留在堆上。恢复 = 线程重新接管这个链表,从上次停的地方继续。