• 在 JNI 编程中避免内存泄漏


    JAVA 中的内存泄漏

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

    Java Heap 的内存泄漏

    Java 对象存储在 JVM 进程空间中的 Java Heap 中,Java Heap 可以在 JVM 运行过程中动态变化。如果 Java 对象越来越多,占据 Java Heap 的空间也越来越大,JVM 会在运行时扩充 Java Heap 的容量。如果 Java Heap 容量扩充到上限,并且在 GC 后仍然没有足够空间分配新的 Java 对象,便会抛出 out of memory 异常,导致 JVM 进程崩溃。

    Java Heap 中 out of memory 异常的出现有两种原因——①程序过于庞大,致使过多 Java 对象的同时存在;②程序编写的错误导致 Java Heap 内存泄漏。

    多种原因可能导致 Java Heap 内存泄漏。JNI 编程错误也可能导致 Java Heap 的内存泄漏。

    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 Heap 往往还没有达到上限。

    多种原因可能导致 JVM 的 native memory 内存泄漏。例如 JVM 在运行中过多的线程被创建,并且在同时运行。JVM 为线程分配的资源就可能耗尽 native memory 的容量。

    JNI 编程错误也可能导致 native memory 的内存泄漏。对这个话题的讨论是本文的重点。

     

    JNI 编程中明显的内存泄漏

    JNI 编程实现了 native code 和 Java 程序的交互,因此 JNI 代码编程既遵循 native code 编程语言的编程规则,同时也遵守 JNI 编程的文档规范。在内存管理方面,native code 编程语言本身的内存管理机制依然要遵循,同时也要考虑 JNI 编程的内存管理。

    本章简单概括 JNI 编程中显而易见的内存泄漏。从 native code 编程语言自身的内存管理,和 JNI 规范附加的内存管理两方面进行阐述。

    Native Code 本身的内存泄漏

    JNI 编程首先是一门具体的编程语言,或者 C 语言,或者 C++,或者汇编,或者其它 native 的编程语言。每门编程语言环境都实现了自身的内存管理机制。因此,JNI 程序开发者要遵循 native 语言本身的内存管理机制,避免造成内存泄漏。以 C 语言为例,当用 malloc() 在进程堆中动态分配内存时,JNI 程序在使用完后,应当调用 free() 将内存释放。总之,所有在 native 语言编程中应当注意的内存泄漏规则,在 JNI 编程中依然适应。

    Native 语言本身引入的内存泄漏会造成 native memory 的内存,严重情况下会造成 native memory 的 out of memory。

    Global Reference 引入的内存泄漏

    JNI 编程还要同时遵循 JNI 的规范标准,JVM 附加了 JNI 编程特有的内存管理机制。

    JNI 中的 Local Reference 只在 native method 执行时存在,当 native method 执行完后自动失效。这种自动失效,使得对 Local Reference 的使用相对简单,native method 执行完后,它们所引用的 Java 对象的 reference count 会相应减 1。不会造成 Java Heap 中 Java 对象的内存泄漏。

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

     

    Local Reference 在 native method 执行完成后,会自动被释放,似乎不会造成任何的内存泄漏。但这是错误的。对 Local Reference 的理解不够,会造成潜在的内存泄漏。

    本章重点阐述 Local Reference 使用不当可能引发的内存泄漏。引入两个错误实例,也是 JNI 程序员容易忽视的错误;在此基础上介绍 Local Reference 表,对比 native method 中的局部变量和 JNI Local Reference 的不同,使读者深入理解 JNI Local Reference 的实质;最后为 JNI 程序员提出应该如何正确合理使用 JNI Local Reference,以避免内存泄漏。

    错误实例 1

    在某些情况下,我们可能需要在 native method 里面创建大量的 JNI Local Reference。这样可能导致 native memory 的内存泄漏,如果在 native method 返回之前 native memory 已经被用光,就会导致 native memory 的 out of memory。

    在代码清单 1 里,我们循环执行 count 次,JNI function NewStringUTF() 在每次循环中从 Java Heap 中创建一个 String 对象,str 是 Java Heap 传给 JNI native method 的 Local Reference,每次循环中新创建的 String 对象覆盖上次循环中 str 的内容。str 似乎一直在引用到一个 String 对象。整个运行过程中,我们看似只创建一个 Local Reference。

    执行代码清单 1 的程序,第一部分为 Java 代码,nativeMethod(int i) 中,输入参数设定循环的次数。第二部分为 JNI 代码,用 C 语言实现了 nativeMethod(int i)。

    清单 1. Local Reference 引发内存泄漏
     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 代码,nativeMethod(int i) 的 C 语言实现
     #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)

    运行结果证明,JVM 运行异常终止,原因是创建了过多的 Local Reference,从而导致 out of memory。实际上,nativeMethod 在运行中创建了越来越多的 JNI Local Reference,而不是看似的始终只有一个。过多的 Local Reference,导致了 JNI 内部的 JNI Local Reference 表内存溢出。

    错误实例 2

    实例 2 是实例 1 的变种,Java 代码未作修改,但是 nativeMethod(int i) 的 C 语言实现稍作修改。在 JNI 的 native method 中实现的 utility 函数中创建 Java 的 String 对象。utility 函数只建立一个 String 对象,返回给调用函数,但是 utility 函数对调用者的使用情况是未知的,每个函数都可能调用它,并且同一函数可能调用它多次。在实例 2 中,nativeMethod 在循环中调用 count 次,utility 函数在创建一个 String 对象后即返回,并且会有一个退栈过程,似乎所创建的 Local Reference 会在退栈时被删除掉,所以应该不会有很多 Local Reference 被创建。实际运行结果并非如此。

    清单 2. Local Reference 引发内存泄漏
    Java 代码部分参考实例 1,未做任何修改。
    
     JNI 代码,nativeMethod(int i) 的 C 语言实现
     #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; 
     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)

    运行结果证明,实例 2 的结果与实例 1 的完全相同。过多的 Local Reference 被创建,仍然导致了 JNI 内部的 JNI Local Reference 表内存溢出。实际上,在 utility 函数 CreateStringUTF(JNIEnv * env)

    执行完成后的退栈过程中,创建的 Local Reference 并没有像 native code 中的局部变量那样被删除,而是继续在 Local Reference 表中存在,并且有效。Local Reference 和局部变量有着本质的区别。

    Local Reference 深层解析

    Java JNI 的文档规范只描述了 JNI Local Reference 是什么(存在的目的),以及应该怎么使用 Local Reference(开放的接口规范)。但是对 Java 虚拟机中 JNI Local Reference 的实现并没有约束,不同的 Java 虚拟机有不同的实现机制。这样的好处是,不依赖于具体的 JVM 实现,有好的可移植性;并且开发简单,规定了“应该怎么做、怎么用”。但是弊端是初级开发者往往看不到本质,“不知道为什么这样做”。对 Local Reference 没有深层的理解,就会在编程过程中无意识的犯错。

    Local Reference 和 Local Reference 表

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

    JNI Local Reference 的生命期是在 native method 的执行期(从 Java 程序切换到 native code 环境时开始创建,或者在 native method 执行时调用 JNI function 创建),在 native method 执行完毕切换回 Java 程序时,所有 JNI Local Reference 被删除,生命期结束(调用 JNI function 可以提前结束其生命期)。

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

    图 1. Local Reference 表、Local Reference 和 Java 对象的关系

    图 1 中:

    ⑴运行 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 的崩溃。

    Local Ref 不是 native code 的局部变量

    很多人会误将 JNI 中的 Local Reference 理解为 Native Code 的局部变量。这是错误的。

    Native Code 的局部变量和 Local Reference 是完全不同的,区别可以总结为:

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

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

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

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

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

    图 2. str 间接引用 string 对象

    图 2 中,str 是局部变量,在 native method 堆栈中。Local Ref3 是新创建的 Local Reference,在 Local Ref 表中,引用新创建的 String 对象。JNI 通过 str 和指针 p 间接定位 Local Ref3,但 p 和 Local Ref3 对 JNI 程序员不可见。

    Local Reference 导致内存泄漏

    在以上论述基础上,我们通过分析错误实例 1 和实例 2,来分析 Local Reference 可能导致的内存泄漏,加深对 Local Reference 的深层理解。

    分析错误实例 1:

    局部变量 str 在每次循环中都被重新赋值,间接指向最新创建的 Local Reference,前面创建的 Local Reference 一直保留在 Local Ref 表中。

    在实例 1 执行完第 i 次循环后,内存布局如图 3:

    图 3. 执行 i 次循环后的内存布局

    继续执行完第 i+1 次循环后,内存布局发生变化,如图 4:

    图 4. 执行 i+1 次循环后的内存布局

    图 4 中,局部变量 str 被赋新值,间接指向了 Local Ref i+1。在 native method 运行过程中,我们已经无法释放 Local Ref i 占用的内存,以及 Local Ref i 所引用的第 i 个 string 对象所占据的 Java Heap 内存。所以,native memory 中 Local Ref i 被泄漏,Java Heap 中创建的第 i 个 string 对象被泄漏了。

    也就是说在循环中,前面创建的所有 i 个 Local Reference 都泄漏了 native memory 的内存,创建的所有 i 个 string 对象都泄漏了 Java Heap 的内存。

    直到 native memory 执行完毕,返回到 Java 程序时(N2J),这些泄漏的内存才会被释放,但是 Local Reference 表所分配到的内存往往很小,在很多情况下 N2J 之前可能已经引发严重内存泄漏,导致 Local Reference 表的内存耗尽,使 JVM 崩溃,例如错误实例 1。

    分析错误实例 2:

    实例 2 与实例 1 相似,虽然每次循环中调用工具函数 CreateStringUTF(env) 来创建对象,但是在 CreateStringUTF(env) 返回退栈过程中,只是局部变量被删除,而每次调用创建的 Local Reference 仍然存在 Local Ref 表中,并且有效引用到每个新创建的 string 对象。str 局部变量在每次循环中被赋新值。

    这样的内存泄漏是潜在的,但是这样的错误在 JNI 程序员编程过程中却经常出现。通常情况,在触发 out of memory 之前,native method 已经执行完毕,切换回 Java 环境,所有 Local Reference 被删除,问题也就没有显露出来。但是某些情况下就会引发 out of memory,导致实例 1 和实例 2 中的 JVM 崩溃。

    控制 Local Reference 生命期

    因此,在 JNI 编程时,正确控制 JNI Local Reference 的生命期。如果需要创建过多的 Local Reference,那么在对被引用的 Java 对象操作结束后,需要调用 JNI function(如 DeleteLocalRef()),及时将 JNI Local Reference 从 Local Ref 表中删除,以避免潜在的内存泄漏。

     


    常见问题:

    一、jni调用java对象

      JNI提供的功能之一是在本地代码中使用Java对象。包括:创建一个java类对象和通过函数传递一个java对象。创建一个java类对象,首先需要得到得到使用FindClass/GetObjectClass函数得到该类,然后使用GetMethodID方法得到该类的方法id,然后调用该函数。 Java 和 Native 代码之间函数调用时,如果是简单类型,也就是内置类型,比如 int, char 等是值传递(pass by value),而其它 Java 对象都是引用传递(pass by reference),这些对象引用由 JVM 传给 Native 代码。  

      在本地方法中调用Java对象的方法的步骤:  

      1)获取你需要访问的Java对象的类  

        FindClass通过传java中完整的类名来查找java的class

        GetObjectClass通过传入jni中的一个java的引用来获取该引用的类型。  

        他们之间的区别是,前者要求你必须知道完整的类名,后者要求在Jni有一个类的引用。  

      2)获取MethodID,调用方法  

        GetMethodID 得到一个实例的方法的ID  

        GetStaticMethodID 得到一个静态方法的ID  

      3)获取对象的属性  

        GetFieldID 得到一个实例的域的ID  

        GetStaticFieldID 得到一个静态的域的ID  

        JNI通过ID识别域和方法,一个域或方法的ID是任何处理域和方法的函数的必须参数。  

    二、jni中引用的java对象的生命周期

      Java对象做为引用被传递到本地方法中,所有这些Java对象的引用都有一个共同的父类型jobject(相当于java中的 Object类是所有类的父类一样)。 这些对象引用都有其生命周期。在JNI中对Java对象的引用根据生命周期分为:全局引用,局部引用、弱全局引用  

      1、Local Reference 本地引用,  

        函数调用时传入jobject或者jni函数创建的jobejct,都是本地引用.  

        其特点就是一旦JNI层函数返回,jobject就被垃圾回收掉,所以需要注意其生命周期。可以强制调用DeleteLocalRef进行立即回收。  

        jstring pathStr = env->NewStringUTF(path)  

        ....  

        env->DeleteLocalRef(pathStr);  

      2、Global Reference 全局引用 ,这种对象如不主动释放,它永远都不会被垃圾回收  

        创建: env->NewGlobalRef(obj);  

        释放: env->DeleteGlobalRef(obj)  

        若要在某个 Native 代码返回后,还希望能继续使用 JVM 提供的参数, 或者是过程中调用 JNI 函数的返回值(比如 g_mid), 则将该对象设为 global       reference,以后只能使用这个 global reference;若不是一个 jobject,则无需这么做。  

      3、Weak Global Reference 弱全局引用  

        一种特殊的 Global Reference ,在运行过程中可能被垃圾回收掉,所以使用时请务必注意其生命周期及随时可能被垃圾回收掉,比如内存不足时。  

        使用前可以利用JNIEnv的 IsSameObject 进行判定它是否被回收  

        env->IsSameObject(obj1,obj2);  

    三、本地线程中调用java对象

      问题1:  

        JNIEnv是一个线程相关的变量  

        JNIEnv 对于每个 thread 而言是唯一的  

        JNIEnv *env指针不可以为多个线程共用  

    解决办法:  

      但是java虚拟机的JavaVM指针是整个jvm公用的,我们可以通过JavaVM来得到当前线程的JNIEnv指针.  

      可以使用javaAttachThread保证取得当前线程的Jni环境变量  

      static JavaVM *gs_jvm=NULL;  
      gs_jvm->AttachCurrentThread((void **)&env, NULL);//附加当前线程到一个Java虚拟机  
      jclass cls = env->GetObjectClass(gs_object);  
      jfieldID fieldPtr = env->GetFieldID(cls,"value","I");  

    问题2:  

      不能直接保存一个线程中的jobject指针到全局变量中,然后在另外一个线程中使用它。  

    解决办法:  

      用env->NewGlobalRef创建一个全局变量,将传入的obj(局部变量)保存到全局变量中,其他线程可以使用这个全局变量来操纵这个java对象  

      注意:若不是一个 jobject,则不需要这么做。如:  

      jclass 是由 jobject public 继承而来的子类,所以它当然是一个 jobject,需要创建一个 global reference 以便日后使用。  

      而 jmethodID/jfieldID 与 jobject 没有继承关系,它不是一个 jobject,只是个整数,所以不存在被释放与否的问题,可保存后直接使用。  

    static jobject gs_object=NULL;  
    JNIEXPORT void JNICALL Java_Test_setEnev(JNIEnv *env, jobject obj)  
    {  
      env->GetJavaVM(&gs_jvm); //保存到全局变量中JVM  
    
      //直接赋值obj到全局变量是不行的,应该调用以下函数:  
      gs_object=env->NewGlobalRef(obj);  
    }  

    参考资料

    学习

    讨论

    • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。
  • 相关阅读:
    Javascript入门(三)函数
    Javascript入门(二)变量、获取元素、操作元素
    Javascript入门(一)弹出方框
    Linux常用命令(二)查找当前ip地址
    python笔记(一)获取当前目录路径和文件
    Linux常用命令(一)查看日志
    产品对话 | 愿云原生不再只有Kubernete
    在线公开课 | 5G时代的视频云服务关键技术与实践
    IT培训行业变革大会,7月11日启程!
    业内首发 | 区块链数据服务
  • 原文地址:https://www.cnblogs.com/yaozhongxiao/p/3781465.html
Copyright © 2020-2023  润新知