Skip to content

字节码存储: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 都很重要。尽量避免在挂起点之间持有不必要的对象。