Kotlin构造函数对象生命周期与EBADF错误排查
问题背景
我在Kotlin中调用一个以Unix文件描述符为参数的原生函数,运行数分钟后出现EBADF错误。代码示例如下:
class A(val file: ParcelFileDescriptor) : AutoCloseable { private var fileDes: Int = -1 private external fun longRunningNativeFn(fd : Int) : FnResult init { fileDes = file.fd } fun process() : FnResult { return longRunningNativeFn(fileDes) } override fun close() { } }
传入构造函数的file对象未被其他地方持有。我的推测是:由于file仅在init块中使用,会被标记为垃圾回收候选对象,当垃圾回收器执行时file对象被关闭,导致原生代码获取无效文件描述符。现咨询以下问题:
- 该推测是否正确?
- 构造函数参数的生命周期由什么决定?
- 在
close函数中添加file.close()能否延长file的生命周期至类实例存续期?
补充说明:添加file.close()后,原生代码不再出现EBADF错误。尽管@Ivo指出主构造函数的val使file成为类成员,但JRE可能因检测到file未被使用而提前回收它,而我们需要它存续至close调用时。
问题解答
1. 该推测是否正确?
正确。虽然主构造函数的val file会将file设为类的成员变量,但JVM的逃逸分析如果检测到这个成员变量在类的后续逻辑中没有被任何代码引用,就会判定它可以被提前回收。而ParcelFileDescriptor作为资源管理类,其内部的清理逻辑(比如finalize方法)会在对象被GC时自动关闭关联的文件描述符,这就导致原生函数持有的fd变成无效状态,进而触发EBADF错误。
2. 构造函数参数的生命周期由什么决定?
构造函数参数的生命周期由两个核心因素决定:
- 持有情况:如果参数仅在构造函数/init块中使用,没有被保存到类成员、外部变量或传递到其他作用域,那么它的生命周期会在构造函数执行完成后结束,成为GC候选对象;如果被赋值给类成员,生命周期会和类实例绑定,但存在被提前优化回收的可能。
- JVM逃逸分析:即使参数被赋值给类成员,若JVM检测到该成员在类实例的整个生命周期中没有被任何方法引用,会进行激进优化,提前回收该成员对应的对象,哪怕类实例本身还存活。只有当参数(或其赋值的类成员)在类实例生命周期内被主动使用(比如调用方法、访问属性),JVM才会保证其生命周期至少持续到使用场景结束。
3. 在close函数中添加file.close()能否延长file的生命周期至类实例存续期?
可以。当你在close()方法中调用file.close()时,相当于在类实例的生命周期末期主动引用了file成员变量。这会让JVM的逃逸分析认为file在类实例的整个生命周期内都有被使用的需求,从而不会提前回收file对象,直到类实例被GC(或者close()方法执行完成后,类实例不再被持有)。这样ParcelFileDescriptor就不会被提前关闭,原生函数持有的文件描述符就能保持有效直到close()被调用。
内容的提问来源于stack exchange,提问作者doron

