使用 JNI 的内存泄漏:我们是否正在释放对象 属性?

Memory leaks using JNI: are we releasing objects property?

重要说明:这段代码不是从 Java 调用的 native 函数。我们的流程是用 C++ 编写的,我们实例化一个 Java VM,我们从 C++ 调用 Java 函数,但绝不会反过来:Java 方法从不调用本机函数,而是调用本机函数实例化 Java 个对象并调用 Java 个函数。

我们正在做这样的事情:

void some_fun()
{
    // env is a JNIEnv*, cls (and cls2) a jclass, and init (and mid) a jmethodID
    jobject obj = env->NewObject(cls, init);
    fill_obj(obj, cpp_data);
    env->callStaticVoidMethod(cls2, mid, obj);
    // env->DeleteLocalRef(obj); // Added out of desperation.
}

其中 fill_obj 取决于必须设置为 obj 字段的 cpp_data 类型。例如,如果 Java class cls 包含一个 ArrayList,则 cpp_data 将包含一个 std::vector。所以 fill_obj 重载看起来像这样:

void fill_obj(jobject obj, SomeType const& cpp_data)
{
   std::vector<SomeSubType> const& v = cpp_data.inner_data;
   jobject list_obj = env->NewObject(array_list_class_global_ref, its_init_method);

   for (auto it = v.begin(); it != v.end(); ++it) {
      jobject child_obj = env->NewObject(SomeSubType_class_global_ref, its_init_method);
      fill_obj(child_obj , *it);
      env->CallBooleanMethod(list_obj, add_method, child_obj);
   }

   env->SetObjectField(obj, field_id, list_obj);
}

根据我们对 JNI 文档的理解,该代码不应该有任何内存泄漏,因为当本地方法结束时本地对象被销毁。因此,当第一个 fill_obj 结束时,在 C++ 端不再有对 list_obj 或其任何子项的引用,并且当 some_fun 结束时,在 C++ 中对 obj 的任何引用边也不见了。

我的理解是 jobject 包含某种引用计数器,当该引用计数器达到 0 时,C++ 端将不再引用 Java 对象。如果在 Java 端也没有对对象的引用,那么 Java 的垃圾收集器可以释放该对象占用的资源。

我们有一个方法,在调用时会创建数千个这样的对象,每次调用该方法时,进程在 RAM(常驻内存)中占用的内存都会增加 200 MiB 以上,并且内存永远不会释放。

我们添加了对 DeleteLocalRef 的显式调用,但结果是一样的。

这是怎么回事?我们是不是做错了什么?

注意 JNI 规范中的 Implementing Local References 部分:

To implement local references, the Java VM creates a registry for each transition of control from Java to a native method. A registry maps nonmovable local references to Java objects, and keeps the objects from being garbage collected. All Java objects passed to the native method (including those that are returned as the results of JNI function calls) are automatically added to the registry. The registry is deleted after the native method returns, allowing all of its entries to be garbage collected.

术语“在本地方法returns之后”意味着“控制权从Java转移到本地方法”的逆转,换句话说,在[=内部声明的方法35=] class 使用关键字 native 已被调用并 returns 给调用者。

理解了这一点,Global and Local References部分的阅读方式也就清楚了

Local references are valid for the duration of a native method call, and are automatically freed after the native method returns.

因此,如果您的代码在返回到 Java 调用方之前运行了很长时间,或者根本没有 Java 调用方,则无法使用 [=21 手动删除引用=].

正如 Remy Lebeau 提到的,您还可以使用 PushLocalFrame and PopLocalFrame 对两点之间创建的引用执行批量销毁。

注意销毁引用并不意味着销毁被引用的对象,只是不阻止对象的垃圾回收。所以在调用 env -> CallBooleanMethod(list_obj, add_method, child_obj); 之后,你可以销毁 child_obj 引用,因为这个对象仍然被你通过 list_obj 引用的列表引用,所以添加的对象将被保留只要它仍在使用中。

既然你说你的 some_fun() 函数没有被 Java 代码调用,它只是在内部调用 JNI 的纯 C++ 代码,那么是的,你需要调用 DeleteLocalRef() 在您持有的任何本地引用上 Java 对象,您不会传回给 Java。您还需要在 for 循环中执行此操作。

试试这个:

void some_fun()
{
    // env is a JNIEnv*, cls (and cls2) a jclass, and init (and mid) a jmethodID
    jobject obj = env->NewObject(cls, init); // <-- local ref created
    fill_obj(obj, cpp_data);
    env->callStaticVoidMethod(cls2, mid, obj);
    env->DeleteLocalRef(obj); // <-- local ref released
}

void fill_obj(jobject obj, SomeType const& cpp_data)
{
   std::vector<SomeSubType> const& v = cpp_data.inner_data;

   jobject list_obj = env->NewObject(array_list_class_global_ref, its_init_method); // <-- local ref created

   for (auto it = v.begin(); it != v.end(); ++it) {
      jobject child_obj = env->NewObject(SomeSubType_class_global_ref, its_init_method); // <-- local ref created
      fill_obj(child_obj, *it);
      env->CallBooleanMethod(list_obj, add_method, child_obj);
      env->DeleteLocalRef(child_obj); // <-- local ref released
   }

   env->SetObjectField(obj, field_id, list_obj);
   env->DeleteLocalRef(list_obj); // <-- local ref released
}

或者,您可以使用 (Push|Pop)LocalFrame() 代替,这样 JNI 可以跟踪您创建的所有本地引用,然后一次性为您释放它们,例如:

void some_fun()
{
    // env is a JNIEnv*, cls (and cls2) a jclass, and init (and mid) a jmethodID

    env->PushLocalFrame(1);

    jobject obj = env->NewObject(cls, init);
    fill_obj(obj, cpp_data);
    env->callStaticVoidMethod(cls2, mid, obj);

    env->PopLocalFrame(NULL);
}

void fill_obj(jobject obj, SomeType const& cpp_data)
{
   std::vector<SomeSubType> const& v = cpp_data.inner_data;

   env->PushLocalFrame(1+v.size());

   jobject list_obj = env->NewObject(array_list_class_global_ref, its_init_method);

   for (auto it = v.begin(); it != v.end(); ++it) {
      jobject child_obj = env->NewObject(SomeSubType_class_global_ref, its_init_method);
      fill_obj(child_obj, *it);
      env->CallBooleanMethod(list_obj, add_method, child_obj);
   }

   env->SetObjectField(obj, field_id, list_obj);

   env->PopLocalFrame(NULL);
}