真正理解LocalRef



1 JVM 中 native memory 的内存泄漏


JVM进程空间中,Java Heap以外的内存空间称为JVM 的 native memory。


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



2 JNI 编程中明显的内存泄漏


在内存管理方面,


native code 编程语言本身的内存管理机制依然要遵循,(malloc/free)


同时也要考虑 JNI 编程的内存管理。(NewLocalRef/DeleteLocalRef)    (GetStringUTFChars/ReleaseStringUTFChars)



3 JNI 编程的内存管理


(1)Global Reference 引入的内存泄漏


程序员在使用 Global Reference 时,需要仔细维护对 Global Reference 的使用。


如果一定要使用 Global Reference,务必确保在不用的时候删除。


否则,Global Reference 引用的 Java 对象将永远停留在 Java Heap 中,造成 Java Heap 的内存泄漏。



(2)JNI 编程中潜在的内存泄漏——对 LocalReference 的深入理解


"Local Reference 在 native method 执行完成后,会自动被释放,似乎不会造成任何的内存泄漏"


(i)引入两个错误实例,也是 JNI 程序员容易忽视的错误;


(ii)在此基础上介绍 Local Reference 表,对比 native method 中的局部变量和 JNI Local Reference 的不同,使读者深入理解 JNI Local Reference 的实质;最后为 JNI 程序员提出应该如何正确合理使用 JNI Local Reference,以避免内存泄漏。


(i)错误实例


(a)错误实例1 


我们可能需要在 native method 里面创建大量的 JNI Local Reference,这样可能导致 native memory 的内存泄漏。


Java代码
class TestLocalReference {
    private native void nativeMethod(int i);
    public static void main(String args[]) {
         TestLocalReference c = new TestLocalReference();
         //call the jni native method
         c.nativeMethod(1000000);
    }
    static {
        //load the jni library
        System.loadLibrary("StaticMethodCall");
    }
}

JNI代码
#include<stdio.h>
#include<jni.h>
#include"TestLocalReference.h"
JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod
(JNIEnv * env, jobject obj, jint count)
{
    jint i = 0;
    jstring str;
    for(; i<count; i++)
         str = (*env)->NewStringUTF(env, "0");
}

运行结果
JVMCI161: FATAL ERROR in native method: Out of memory when expanding
local ref table beyond capacity
at TestLocalReference.nativeMethod(Native Method)
at TestLocalReference.main(TestLocalReference.java:9)


理解Local Reference:


JNI function NewStringUTF() 在每次循环中从 Java Heap 中创建一个 String 对象,


str 是 Java Heap 传给 JNI native method 的 Local Reference,


每次循环中新创建的 String 对象覆盖上次循环中 str 的内容。


str 似乎一直在引用到一个 String 对象。整个运行过程中,我们看似只创建一个 Local Reference。


实际上,nativeMethod 在运行中创建了越来越多的 JNI Local Reference,而不是看似的始终只有一个。过多的 Local Reference,导致了 JNI 内部的 JNI Local Reference 表内存溢出。



(b)错误实例2 


在 JNI 的 native method 中实现的 utility 函数中创建 Java 的 String 对象。utility 函数只建立一个 String 对象,返回给调用函数,但是 utility 函数对调用者的使用情况是未知的,每个函数都可能调用它,并且同一函数可能调用它多次。



Java代码部分参考实例 1,未做任何修改。
JNI代码:
#include<stdio.h>
#include<jni.h>
#include"TestLocalReference.h"
jstring CreateStringUTF(JNIEnv * env)
{
    return (*env)->NewStringUTF(env, "0");
}
JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod
(JNIEnv * env, jobject obj, jint count)
{
    jint i = 0;
    jstring str;
    for(; i<count; i++)
    {
         str = CreateStringUTF(env);
    }
}
运行结果
JVMCI161: FATAL ERROR in native method: Out of memory when expanding local ref
table beyond  capacity
at TestLocalReference.nativeMethod(Native Method)
at TestLocalReference.main(TestLocalReference.java:9)


似乎所创建的 Local Reference 会在退栈时被删除掉,所以应该不会有很多 Local Reference 被创建。实际运行结果并非如此。



运行结果证明,实例 2 的结果与实例 1 的完全相同。过多的 Local Reference 被创建,仍然导致了 JNI 内部的 JNI Local Reference 表内存溢出。


实际上,在 utility 函数 CreateStringUTF(JNIEnv * env)执行完成后的退栈过程中,创建的 Local Reference 并没有像 native code 中的局部变量那样被删除,而是继续在 Local Reference 表中存在,并且有效。



Local Reference 和局部变量有着本质的区别。



(ii) Local Reference 深层解析


(a)Local Reference 和 Local Reference 表


理解 Local Reference 表的存在是理解 JNI Local Reference 的关键


每当线程从 Java 环境切换到 native code 上下文时(J2N),JVM 会分配一块内存,创建一个 Local Reference 表,这个表用来存放本次 native method 执行中创建的所有的 Local Reference。每当在 native code 中引用到一个 Java 对象时,JVM 就会在这个表中创建一个 Local Reference。





jemalloc监测内存泄漏_Java



⑴运行 native method 的线程的堆栈记录着 Local Reference 表的内存位置(指针 p)。


⑵ Local Reference 表中存放 JNI Local Reference,实现 Local Reference 到 Java 对象的映射。


⑶ native method 代码间接访问 Java 对象(java obj1,java obj2)。通过指针 p 定位相应的 Local Reference 的位置,然后通过相应的 Local Reference 映射到 Java 对象。


⑷当 native method 引用一个 Java 对象时,会在 Local Reference 表中创建一个新 Local Reference。在 Local Reference 结构中写入内容,实现 Local Reference 到 Java 对象的映射。


⑸ native method 调用 DeleteLocalRef() 释放某个 JNI Local Reference 时,首先通过指针 p 定位相应的 Local Reference 在 Local Ref 表中的位置,然后从 Local Ref 表中删除该 Local Reference,也就取消了对相应 Java 对象的引用(Ref count 减 1)。


⑹当越来越多的 Local Reference 被创建,这些 Local Reference 会在 Local Ref 表中占据越来越多内存。当 Local Reference 太多以至于 Local Ref 表的空间被用光,JVM 会抛出异常,从而导致 JVM 的崩溃。



(b)Local Ref 不是 native code 的局部变量


区别可以总结为:


⑴局部变量存储在线程堆栈中,而 Local Reference 存储在 Local Ref 表中。


⑵局部变量在函数退栈后被删除,而 Local Reference 在调用 DeleteLocalRef() 后才会从 Local Ref 表中删除,并且失效,或者在整个 Native Method 执行结束后自动被删除。


⑶可以在代码中直接访问局部变量,而 Local Reference 的内容无法在代码中直接访问,必须通过 JNI function 间接访问。JNI function 实现了对 Local Reference 的间接访问,JNI function 的内部实现依赖于具体 JVM。



举例:


代码清单 1 中 jstring str = (*env)->NewStringUTF(env, "0");


str 是 jstring 类型的局部变量。对应str,Local Ref 表中会新创建一个 Local Reference,引用到 NewStringUTF(env, "0") 在 Java Heap 中新建的 String 对象。如图 2 所示:



jemalloc监测内存泄漏_Java_02



图 2 中,str 是局部变量,在 native method 堆栈中。


Local Ref3 是新创建的 Local Reference,在 Local Ref 表中,引用新创建的 String 对象。


但 p 和 Local Ref3 对 JNI 程序员不可见。


[1]jstring str---native method stack


[2]Local Ref3---Local Ref Table


[3]java obj---java heap



总结


JNI 编程既可能引发 Java Heap 的内存泄漏,也可能引发 native memory 的内存泄漏,严重的情况可能使 JVM 运行异常终止。JNI 软件开发人员在编程中,应当考虑以下几点,避免内存泄漏:


1 native code本身的内存管理机制依然要遵循。


2 使用 Global reference 时,当 native code 不再需要访问 Global reference 时,应当调用 JNI 函数 DeleteGlobalRef() 删除 Global reference 和它引用的 Java 对象。Global reference 管理不当会导致 Java Heap 的内存泄漏。


3 透彻理解 Local reference,区分 Local reference 和 native code 的局部变量,避免混淆两者所引起的 native memory 的内存泄漏。


4 使用 Local reference 时,如果 Local reference 引用了大的 Java 对象,当不再需要访问 Local reference 时,应当调用 JNI 函数 DeleteLocalRef() 删除 Local reference,从而也断开对 Java 对象的引用。这样可以避免 Java Heap 的 out of memory。


5 使用 Local reference 时,如果在 native method 执行期间会创建大量的 Local reference,当不再需要访问 Local reference 时,应当调用 JNI 函数 DeleteLocalRef() 删除 Local reference。Local reference 表空间有限,这样可以避免 Local reference 表的内存溢出,避免 native memory 的 out of memory。