字节码存储:Locals Spilling
在 suspend 函数执行过程中,为了在恢复时找回之前的变量,编译器必须将栈中跨越挂起点的局部变量移动到堆中(Continuation 对象的字段)。这一过程称为 Locals Spilling。
存储原理
kotlin
suspend fun doWork() {
val hugeData = ByteArray(1024 * 1024 * 10) // 10MB
delay(100) // 挂起点
println(hugeData.size)
}编译器伪代码:
java
class DoWorkContinuation extends ContinuationImpl {
// 局部变量变成了成员字段,存在堆上
ByteArray hugeData;
// ...
}内存泄漏风险
幽灵引用
Continuation 对象一直存活着直到协程结束。如果一个大对象在挂起点之前被创建,即使挂起点后面再也不用它了,编译器生成的字节码可能(取决于具体的优化程度)仍然会在字段中持有它,导致无法被 GC 回收。
典型场景
kotlin
suspend fun processUserAvatar() {
val bitmap = loadBitmap() // 1. 加载大图
upload(bitmap) // 2. 使用大图
// --- 挂起点 ---
// 此时 bitmap 实际上已经不需要了,但在某些旧版编译器或特定逻辑下,
// DO_WORK 状态可能仍持有 bitmap 引用。
delay(10 * 60 * 1000) // 挂起 10 分钟
}优化方案
对于生命周期极长的协程,若其中包含大对象局部变量,建议显式置空或缩小作用域。
kotlin
suspend fun optimized() {
// 方案: 用 run 代码块缩小作用域
run {
val bitmap = loadBitmap()
upload(bitmap)
} // bitmap 在此处出栈,引用消失
delay(10000)
}性能权衡
Locals Spilling 本质上是用堆内存换栈空间。 在海量并发(百万级协程)场景下,每一个字节的 Spilling 都很重要。尽量避免在挂起点之间持有不必要的对象。