【JNI编程】深入理解JNI内存泄漏

3,430 阅读6分钟

JAVA 编程中的内存泄漏,从泄漏的内存位置角度可以分为两种:JVM 中 Java堆的内存泄漏和JVM 内存中 native memory 的内存泄漏。

从操作系统角度看,JVM 在运行时和其它进程没有本质区别。在系统级别上,它们具有同样的调度机制,同样的内存分配方式,同样的内存格局。

JVM 进程空间中,Java Heap 以外的内存空间称为 JVM 的 native memory。进程的很多资源都是存储在 JVM 的 native memory 中,例如载入的代码映像,线程的堆栈,线程的管理控制块,JVM 的静态数据、全局数据等等。也包括 JNI 程序中 native code 分配到的资源。

在 JVM 运行中,多数进程资源从 native memory 中动态分配。当越来越多的资源在 native memory 中分配,占据越来越多 native memory 空间并且达到 native memory 上限时,JVM 会抛出异常,使 JVM 进程异常退出。而此时 Java堆 往往还没有达到上限。

多种原因可能导致 JVM 的 native memory 内存泄漏。例如 JVM 在运行中过多的线程被创建,并且在同时运行。JVM 为线程分配的资源就可能耗尽 native memory 的容量。JNI 编程错误也可能导致 native memory 的内存泄漏。本节关注的正是JNI编程不当导致的内存泄漏。

以下分析示例代码,均在android下实现,开发环境使用Android Studio。

一、Native代码引入的内存泄漏

Native语言(C、C++等)对内存分配回收使用不当会造成 native memory 的内存泄漏,严重情况下会造成 native memory 的内存溢出。所以良好的、严谨的编码风格有助于避免Native代码使用不当造成的内存泄漏问题。这一点没有多少好说的,如果内功不足,则需要加强修炼。

二、全局引用造成的内存泄漏

JNI全局引用对Java对象的引用一直有效,因此它们引用的Java对象会一直存在Java堆中。程序员在使用JNI全局引用时,需要仔细维护对JNI全局引用的使用。如果一定要使用JNI全局引用,务必确保在不用的时候删除。就像在C/C++语言中,调用 malloc()动态分配一块内存之后,调用free()释放一样。否则,JNI全局引用的Java对象将永远停留在Java堆中,造成Java堆的内存泄漏。

对应的JNI函数为:

jobject NewGlobalRef(JNIEnv *env, jobject obj);

创建对obj参数引用的对象的新全局引用。obj参数可以是全局或局部引用。必须通过调用DeleteGlobalRef()显式处理全局引用。

void DeleteGlobalRef(JNIEnv *env, jobject globalRef);

删除globalRef指向的全局引用。

三、局部引用造成的内存泄漏

局部引用在 native method 执行完成后,会自动被释放,似乎不会造成任何的内存泄漏的问题。但在某些情况下,还是可能遇到问题的。

我们来看JNI API中对局部引用的说明,局部引用在本地方法调用的持续时间内有效。它们在本地方法返回后自动释放。每个局部引用都会花费一定量的Java虚拟机资源。程序员需要确保本地方法不会过度分配局部引用。尽管在本地方法返回到Java之后会自动释放局部引用,但过度分配局部引用可能会导致VM在执行本地方法期间耗尽内存。

以下是删除局部引用的方法:

// 删除localRef指向的局部引用。
void DeleteLocalRef(JNIEnv *env, jobject localRef);

请注意

JDK/JRE 1.1提供了上面的DeleteLocalRef函数,程序员可以手动删除局部引用。例如,如果本地代码遍历一个可能很大的对象数组并在每次迭代中使用一个元素,那么在下一次迭代中创建新的局部引用之前,最好删除对不再使用的数组元素的局部引用。

3.1 本地方法中创建大量JNI局部引用

本地方法中创建大量的JNI局部引用。这可能导致 native memory 的内存泄漏,如果在本地方法返回之前 native memory 已经被用光,就会导致 native memory 的内存溢出。

3.1.1 实例一

package com.example.ndk.ndkdemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}

JNI 代码,stringFromJNI()的C++语言实现

#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jstring

JNICALL
Java_com_example_ndk_ndkdemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    jint i = 0;
    jstring str;
    std::string hello = "Hello from C++";
    for(i = 0; i < 1000000; i++)
        str = env->NewStringUTF(hello.c_str());

    return str;
}

运行结果:

01-25 09:21:26.627 3003-3003/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115] JNI ERROR (app bug): local reference table overflow (max=512)
01-25 09:21:26.627 3003-3003/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115] local reference table dump:
01-25 09:21:26.627 3003-3003/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]   Last 10 entries (of 512):
01-25 09:21:26.627 3003-3003/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       511: 0x12e2ea90 java.lang.String "Hello from C++"
01-25 09:21:26.627 3003-3003/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       510: 0x12e2ea60 java.lang.String "Hello from C++"
01-25 09:21:26.627 3003-3003/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       509: 0x12e2ea30 java.lang.String "Hello from C++"
01-25 09:21:26.627 3003-3003/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       508: 0x12e2ea00 java.lang.String "Hello from C++"
01-25 09:21:26.627 3003-3003/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       507: 0x12e2e9d0 java.lang.String "Hello from C++"
01-25 09:21:26.627 3003-3003/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       506: 0x12e2e9a0 java.lang.String "Hello from C++"
01-25 09:21:26.627 3003-3003/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       505: 0x12e2e970 java.lang.String "Hello from C++"
01-25 09:21:26.627 3003-3003/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       504: 0x12e2e940 java.lang.String "Hello from C++"
01-25 09:21:26.627 3003-3003/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       503: 0x12e2e910 java.lang.String "Hello from C++"
01-25 09:21:26.627 3003-3003/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       502: 0x12e2e8e0 java.lang.String "Hello from C++"

我们循环执行1000000次,JNI函数NewStringUTF()在每次循环中从Java堆中创建一个String对象,str是Java堆传给JNI本地方法的局部引用,每次循环中新创建的String对象覆盖上次循环中str的内容。str似乎一直在引用到一个String对象。整个运行过程中,我们看似只创建一个局部引用。

运行结果证明,JVM运行异常终止,原因是创建了过多的局部引用(本地引用表溢出(max=512),可见在Andorid平台(Android 6.0 ART虚拟机)上表最大是512),从而导致内存溢出。实际上,这里本地方法在运行中创建了越来越多的JNI局部引用,而不是看似的始终只有一个。过多的局部引用导致了JNI内部的JNI局部引用表内存溢出。

3.1.2 实例二

修改实例一的代码如下,封装我们自己的NewStringUTF方法供使用。

#include <jni.h>
#include <string>

jstring NewStringUTF(JNIEnv *env, const char *str) {
    return env->NewStringUTF(str);
}

extern "C"
JNIEXPORT jstring
JNICALL
Java_com_example_ndk_ndkdemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    jint i = 0;
    std::string hello = "Hello from C++";
    for (i = 0; i < 1000000; i++)
        jstring str = NewStringUTF(env, hello.c_str());

    return NULL;
}

运行结果

01-25 11:00:53.459 4471-4471/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115] JNI ERROR (app bug): local reference table overflow (max=512)
01-25 11:00:53.459 4471-4471/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115] local reference table dump:
01-25 11:00:53.459 4471-4471/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]   Last 10 entries (of 512):
01-25 11:00:53.459 4471-4471/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       511: 0x12e478e0 java.lang.String "Hello from C++"
01-25 11:00:53.459 4471-4471/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       510: 0x12e478b0 java.lang.String "Hello from C++"
01-25 11:00:53.459 4471-4471/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       509: 0x12e47880 java.lang.String "Hello from C++"
01-25 11:00:53.459 4471-4471/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       508: 0x12e47850 java.lang.String "Hello from C++"
01-25 11:00:53.459 4471-4471/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       507: 0x12e47820 java.lang.String "Hello from C++"
01-25 11:00:53.459 4471-4471/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       506: 0x12e477f0 java.lang.String "Hello from C++"
01-25 11:00:53.459 4471-4471/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       505: 0x12e477c0 java.lang.String "Hello from C++"
01-25 11:00:53.459 4471-4471/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       504: 0x12e47790 java.lang.String "Hello from C++"
01-25 11:00:53.459 4471-4471/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       503: 0x12e47760 java.lang.String "Hello from C++"
01-25 11:00:53.459 4471-4471/com.example.ndk.ndkdemo A/art: art/runtime/indirect_reference_table.cc:115]       502: 0x12e47730 java.lang.String "Hello from C++"

同样造成了局部变量表溢出,按照我们的理解,在方法NewStringUTF(JNIEnv *env, const char *str)入栈出栈以后,局部引用应该释放掉。但最终结果却没有释放,说明JNI中的局部引用和我们想当然的以为是局部变量并非同一概念。

3.2 深入JNI局部引用

可以说理解JNI局部引用的关键就在于理解JNI局部引用表。

JNI局部引用的生命期是在本地方法的执行期(从Java程序切换到本地环境时开始创建,或者在本地方法执行时调用JNI function创建),在本地方法执行完毕切换回Java程序时,所有JNI局部引用被删除,生命期结束(调用JNI function可以提前结束其生命期)。

实际上,每当线程从Java环境切换到本地代码上下文时(J2N),JVM会分配一块内存,创建一个局部引用表,这个表用来存放本次本地方法执行中创建的所有的局部引用。每当在本地代码中引用到一个Java对象时,JVM就会在这个表中创建一个 局部引用。比如,实例一中我们调用NewStringUTF()在Java堆中创建一个String对象后,在局部引用表中就会相应新增一个局部引用。

在这里插入图片描述
上图中:

  • 运行本地方法的线程的堆栈记录着局部引用表的内存位置(指针p);
  • 局部引用表中存放着JNI局部引用,实现局部引用到Java对象的映射;
  • 本地方法代码间接访问Java对象(java obj1,java obj2)。通过指针p定位相应的局部引用的位置,然后通过相应的局部引用映射到Java对象;
  • 当本地方法引用一个Java对象时,会在局部引用表中创建一个新的局部引用。在局部引用表中写入内容,实现局部引用到Java对象的映射;
  • 本地方法调用DeleteLocalRef()释放某个JNI局部引用时,首先通过指针p定位相应的局部引用在局部引用表中的位置,然后从局部引用表表中删除该局部引用,也就取消了对相应Java对象的引用(Ref count 减 1);
  • 当越来越多的局部引用被创建,这些局部引用会在局部引用表中占据越来越多内存。当局部引用太多以至于局部引用表的空间被用光,JVM会抛出异常,从而导致JVM的崩溃。

记住JNI局部引用并不是本地代码中的局部变量。局部变量存储在本地线程堆栈中,而局部引用存储在局部引用表中的。局部变量在函数退栈后被删除,局部引用则不会,直到调用DeleteLocalRef() 或者是整个本地方法执行结束。可以在本地代码中直接访问局部变量,而局部引用的内容无法在代码中直接访问,必须通过JNI function间接访问。JNI function实现了对局部引用的间接访问,JNI function的内部实现依赖于具体JVM。

再去回过头看实例一和实例二,就很容易理解出现问题的原因了。实例一是因为每次创建一个Java堆中的字符串都会在本地方法相应的局部引用表中添加引用,直到表被撑爆。实例二也是类似的,虽然本地方法正常入栈出栈后局部变量会删除,但局部引用表中的引用却并没有实时删除,一样造成了引用表溢出。

在JNI编程中,本地方法如果需要创建过多的局部引用,那么在对被引用的Java对象操作结束后,需要调用DeleteLocalRef()及时删除局部引用,避免造成内存泄露。

参考资料

1.www.ibm.com/developerwo…