
本文详解 JNI 开发中因 jweak 与 finalize() 协同使用导致的 use-after-free 崩溃问题,指出根本原因是 JVM 垃圾回收器在对象 finalize 阶段仍可能通过 jweak 恢复强引用,造成 C++ 端资源已被释放却仍在被 Java 调用。推荐弃用 NewWeakGlobalRef,改用 Java 层 WeakReference 构建线程安全、生命周期可控的弱引用封装。
本文详解 jni 开发中因 `jweak` 与 `finalize()` 协同使用导致的 use-after-free 崩溃问题,指出根本原因是 jvm 垃圾回收器在对象 finalize 阶段仍可能通过 `jweak` 恢复强引用,造成 c++ 端资源已被释放却仍在被 java 调用。推荐弃用 `newweakglobalref`,改用 java 层 `weakreference` 构建线程安全、生命周期可控的弱引用封装。
在混合 Java/C++ 的 JNI 应用中,一个常见但高危的模式是:C++ 缓存 Java 对象的 jweak 引用(通过 NewWeakGlobalRef 创建),并在 Java 侧调用如 getObj(id) 时,用 env->NewLocalRef(weak_obj) 恢复为局部强引用返回。表面看逻辑清晰,实则埋下严重隐患——当该 Java 对象进入 GC 流程并触发 finalize() 时,JVM 并不保证 finalize() 执行完毕后才回收其关联的 native 资源;而此时若 Java 代码再次调用该对象方法(如 obj.methodB()),而 C++ 实现中又依赖已释放的底层资源(例如 reinterpret_cast<fooimpl>(this)</fooimpl>),就会直接触发非法内存访问,导致应用崩溃。
核心问题根源:jweak(尤其是 NewWeakGlobalRef 创建的)的语义与 Java finalize() 的执行时机存在不可控竞争。根据 JVM 规范,jweak 在对象仅剩弱引用时仍可被 NewLocalRef 成功提升为强引用——甚至发生在 finalize() 正在执行、但 native 资源尚未清理完成的“窗口期”。这违反了“对象 finalize 后不应再被安全访问”的隐式契约,本质是 use-after-free。
✅ 正确解法:完全避免在 finalize() 场景下使用 jweak
- 首选方案:彻底弃用
finalize()。自 Java 9 起,finalize()已被标记为@Deprecated(forRemoval=true),且其执行时机不确定、性能差、易引发死锁。应改用Cleaner或PhantomReference配合ReferenceQueue进行确定性资源清理。 - 若必须保留
finalize()(如兼容旧代码),则 绝不可使用jweak关联待 finalize 的对象。此时应改用 Java 标准库的java.lang.ref.WeakReference,它提供明确的生命周期语义:一旦对象进入 finalization 队列,WeakReference.get()必然返回null,从根本上杜绝了“复活”风险。
以下是一个生产就绪的 C++ 封装示例,实现类型安全、自动管理的 WeakRef:
// 初始化(通常在 JNI_OnLoad 中调用)
struct WeakReferenceJniInfo {
GlobalRef<jclass> clazz;
jmethodID constructor;
jmethodID get;
explicit WeakReferenceJniInfo(JNIEnv* env)
: clazz{env, env->FindClass("java/lang/ref/WeakReference")},
constructor{env->GetMethodID(clazz.get(), "<init>", "(Ljava/lang/Object;)V")},
get{env->GetMethodID(clazz.get(), "get", "()Ljava/lang/Object;")} {}
};
inline std::unique_ptr<const weakreferencejniinfo> g_weakReferenceJniInfo;
// 线程安全的 WeakRef 封装
class WeakRef {
std::shared_ptr<globalref>> _weakRef;
public:
WeakRef() = default;
WeakRef(JNIEnv* env, jobject obj)
: _weakRef(std::make_shared<globalref>>(
env, env->NewObject(g_weakReferenceJniInfo->clazz.get(),
g_weakReferenceJniInfo->constructor, obj))) {}
// 安全获取:返回 LocalRef,若对象已回收则返回 nullptr
jobject lock(JNIEnv* env) const {
if (!_weakRef) return nullptr;
jobject result = env->CallObjectMethod(_weakRef->get(), g_weakReferenceJniInfo->get);
// 注意:CallObjectMethod 返回的是 LocalRef,无需 DeleteLocalRef(由 JVM 自动管理)
return result; // caller 负责在使用后调用 DeleteLocalRef
}
// 判断是否指向同一 Java 对象(常用于缓存一致性校验)
bool isSame(JNIEnv* env, jobject obj) const {
auto local = lock(env);
bool same = env->IsSameObject(obj, local);
if (local) env->DeleteLocalRef(local); // 显式清理临时 LocalRef
return same;
}
};</globalref></globalref></const></init></jclass>
关键实践要点:
- ✅
WeakRef::lock()返回的是 Local Reference,Java 侧接收后需及时使用,避免跨 JNI 调用边界长期持有(否则可能触发局部引用表溢出); - ✅ 所有
GlobalRef和WeakRef实例必须确保线程安全访问(本例中std::shared_ptr保障了_weakRef的线程安全析构); - ✅ 在 Java 层,
WeakReference的get()方法在对象 finalize 后严格返回null,这是 JVM 提供的强保证,远比jweak可靠; - ⚠️ 禁止在
finalize()方法内调用任何 JNI 函数或访问jobject—— 此时对象状态已不可信。
总结:JNI 互操作中的内存安全,本质是两端生命周期语义的对齐。jweak 设计初衷是解决跨线程引用传递,而非替代 Java 的引用队列机制。拥抱 java.lang.ref.* 体系,配合现代 C++ RAII 管理(如 std::unique_ptr 封装 GlobalRef),才能构建健壮、可维护的混合栈应用。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











