JNI 编程指南10——从内存角度看引用类型

目录

  • [一、Java 程序使用的内存](#一、Java 程序使用的内存)
  • [二、 内存角度的 JNI 引用类型](#二、 内存角度的 JNI 引用类型)
    • [2.1 局部引用(Local Reference)](#2.1 局部引用(Local Reference))
    • [2.2 全局引用(Global Reference)](#2.2 全局引用(Global Reference))
    • [2.3 弱全局引用(Weak Global Reference)](#2.3 弱全局引用(Weak Global Reference))
  • 三、参考资料

|----------------|
| 从内存角度看引用类型 |

一、Java 程序使用的内存

Java 程序使用的内存从逻辑上可以分为两个部分:

  • Java Memory
  • Native Memory

Java Memory 就是我们的 Java 程序使用的内存,通常从逻辑上区分为栈和堆。方法中的局部变量通常存储在栈中,引用类型指向的对象一般存储在堆中。Java Memory 由 JVM 分配和管理,JVM 中通常会有一个 GC 线程,用于回收不再使用的内存。

Java 程序的执行依托于 JVM ,JVM 一般使用 C/C++ 代码编写,需要根据 Native 编程规范去操作内存。如:C/C++ 使用 malloc()/new 分配内存,需要手动使用 free()/delete 回收内存。这部分内存我们称为 Native Memory。

Java 中的对象对应的内存,由 JVM 来管理,他们都有自己的数据结构。当我们通过 JNI 将一个 Java 对象传递给 Native 程序时,Native 程序要操作这块内存时(即操作这个对象),就需要了解这个数据结构,显然这有点麻烦了,所以 JVM 的设计者在 JNIenv 中定义了很多函数(NewStringUTF,FindClass,NewObject 等)来帮你操作和构造这些对象。同时也提供了引用类型(jobject、jstring、jclass、jarray、jintArray等)来引用这些对象。


二、 内存角度的 JNI 引用类型

之前介绍了,JNI 引用类型有三种:Local Reference、Global Reference、Weak Global Reference。接下来我们就从内存的角度来进一步解析这三类引用。

首先,我们需要明确的是引用类型是指针,指向的是 Java 中的对象在 JVM 中对应的内存。引用类型的定义如下:

c 复制代码
#ifdef __cplusplus

class _jobject {};
class _jclass : public _jobject {};
class _jthrowable : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jobjectArray : public _jarray {};

typedef _jobject *jobject;
typedef _jclass *jclass;
typedef _jthrowable *jthrowable;
typedef _jstring *jstring;
typedef _jarray *jarray;
typedef _jbooleanArray *jbooleanArray;
typedef _jbyteArray *jbyteArray;
typedef _jcharArray *jcharArray;
typedef _jshortArray *jshortArray;
typedef _jintArray *jintArray;
typedef _jlongArray *jlongArray;
typedef _jfloatArray *jfloatArray;
typedef _jdoubleArray *jdoubleArray;
typedef _jobjectArray *jobjectArray;

#else

struct _jobject;

typedef struct _jobject *jobject;
typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;

#endif

不是以上类型的指针就不是 JNI 引用类型,比如容易混淆的 jmethod jfield 都不是 JNI 引用类型

JNI 引用类型是指针,但是和 C/C++ 中的普通指针不同,C/C++ 中的指针需要我们自己分配和回收内存(C/C++ 使用 malloc()/new 分配内存,需要手动使用 free()/delete 回收内存)。JNI 引用不需要我们分配和回收内存,这部分工作由 JVM 完成。我们额外需要做的工作是在 JNI 引用类型使用完后,将其从引用表中删除,防止引用表满了。

接下来我们就从内存角度分类解析三种类型引用类型。

2.1 局部引用(Local Reference)

通过 JNI 接口从 Java 传递下来或者通过 NewLocalRef 和各种 JNI 接口(FindClass、NewObject、GetObjectClass和NewCharArray等)创建的引用称为局部引用。

当从 Java 环境切换到 Native 环境时,JVM 分配一块内存用于创建一个 Local Reference Table,这个 Table 用来存放本次 Native Method 执行中创建的所有局部引用(Local Reference)。每当在 Native 代码中引用到一个 Java 对象时,JVM 就会在这个 Table 中创建一个 Local Reference。比如,我们调用 NewStringUTF() 在 Java Heap 中创建一个 String 对象后,在 Local Reference Table 中就会相应新增一个 Local Reference。

对于开发者来说,Local Reference Table 是不可见的,Local Reference Table 的内存不大,所能存放的 Local Reference 数量也是有限的(在 Android 中默认最大容量是512个)。在开发中应该及时使用 DeleteLocalRef( )删除不必要的 Local Reference,不然可能会出现溢出错误。

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

  • 局部变量存储在线程堆栈中,而 Local Reference 存储在 Local Ref 表中。
  • 局部变量在函数退栈后被删除,而 Local Reference 在调用 DeleteLocalRef() 后才会从 Local Ref 表中删除,并且失效,或者在整个 Native Method 执行结束后被删除。
  • 可以在代码中直接访问局部变量,而 Local Reference 的内容无法在代码中直接访问,必须通过 JNI function 间接访问。JNI function 实现了对 Local Reference 的间接访问,JNI function 的内部实现依赖于具体 JVM。

2.2 全局引用(Global Reference)

Global Reference 是通过 JNI 函数 NewGlobalRef() 和D eleteGlobalRef() 来创建和删除的。 Global Reference 具有全局性,可以在多个 Native Method 调用过程和多线程中使用。

使用 Global reference时,当 native code 不再需要访问 Global reference 时,应当调用 JNI 函数 DeleteGlobalRef() 删除 Global reference 和它引用的 Java 对象。否则 Global Reference 引用的 Java 对象将永远停留在 Java Heap 中,从而导致 Java Heap 的内存泄漏。

2.3 弱全局引用(Weak Global Reference)

弱全局引用使用 NewWeakGlobalRef() 和 DeleteWeakGlobalRef() 进行创建和删除,它与 Global Reference 的区别在于该类型的引用随时都可能被 GC 回收。对于 Weak Global Reference 而言,可以通过 isSameObject() 将其与 NULL 比较,看看是否已经被回收了。如果返回 JNI_TRUE,则表示已经被回收了,需要重新初始化弱全局引用。Weak Global Reference 的回收时机是不确定的,有可能在前一行代码判断它是可用的,后一行代码就被 GC 回收掉了。为了避免这事事情发生,JNI官方给出了正确的做法,通过 NewLocalRef() 获取 Weak Global Reference,基于弱全局引用创建临时强局部引用,避免被GC回收。

示例如下

c 复制代码
// 头文件引入
#include <jni.h>
#include <android/log.h>

// 定义日志宏(方便调试)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "JNI_WEAK_REF", __VA_ARGS__)

// 全局弱引用(jweak 是弱全局引用的专属类型)
jweak g_weakGlobalRef = NULL;

// 辅助方法:重新初始化弱全局引用
void reinitWeakGlobalRef(JNIEnv *env) {
    // 1. 查找目标类
    jclass clazz = env->FindClass("com/xxx/TargetObject");
    if (clazz == NULL) {
        LOGE("查找 TargetObject 类失败");
        return;
    }
    
    // 2. 获取无参构造方法(方法签名:()V 表示无参、返回值为 void)
    jmethodID constructor = env->GetMethodID(clazz, "<init>", "()V");
    if (constructor == NULL) {
        LOGE("获取构造方法失败");
        env->DeleteLocalRef(clazz); // 释放局部引用,避免内存泄漏
        return;
    }
    
    // 3. 创建 TargetObject 实例对象
    jobject obj = env->NewObject(clazz, constructor);
    if (obj == NULL) {
        LOGE("创建 TargetObject 实例失败");
        env->DeleteLocalRef(clazz); // 释放局部引用
        return;
    }
    
    // 4. 替换旧的弱全局引用(先释放旧的,再创建新的)
    if (g_weakGlobalRef != NULL) {
        env->DeleteWeakGlobalRef(g_weakGlobalRef); // 释放旧的弱全局引用
    }
    // 创建新的弱全局引用(C++ 风格:直接 env->NewWeakGlobalRef())
    g_weakGlobalRef = env->NewWeakGlobalRef(obj);
    
    // 5. 释放局部引用(clazz、obj 都是局部引用,使用完毕手动释放)
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(obj);
    
    if (g_weakGlobalRef == NULL) {
        LOGE("创建弱全局引用失败");
    } else {
        LOGE("弱全局引用重新初始化成功");
    }
}


extern "C" void Java_com_xxx_JNIUtils_operateWeakGlobalRef(JNIEnv *env, jobject thiz) {
    // 步骤1:判断弱全局引用是否已被 GC 回收
    // IsSameObject:返回 JNI_TRUE 表示已回收,JNI_FALSE 表示未回收
    if (env->IsSameObject(g_weakGlobalRef, NULL) == JNI_FALSE) {
        // 步骤2:基于弱全局引用,创建强局部引用(关键:锁住对象,避免操作期间被 GC 回收)
        jobject localRef = env->NewLocalRef(g_weakGlobalRef);
        
        // 步骤3:双重保障:判断局部引用是否创建成功(极端情况下可能创建失败)
        if (localRef != NULL) {
            // 步骤4:安全操作局部引用(实例方法调用示例)
            // 4.1 获取对象的类
            jclass clazz = env->GetObjectClass(localRef);
            if (clazz != NULL) {
                // 4.2 获取实例方法 ID(someMethod() 是 TargetObject 的无参实例方法)
                jmethodID methodId = env->GetMethodID(clazz, "someMethod", "()V");
                if (methodId != NULL) {
                    // 4.3 调用实例方法
                    env->CallVoidMethod(localRef, methodId);
                } else {
                    LOGE("获取 someMethod 方法 ID 失败");
                }
                // 释放 clazz 局部引用
                env->DeleteLocalRef(clazz);
            } else {
                LOGE("获取对象类失败");
            }
            
            // 步骤5:操作完成后,手动释放局部引用(避免局部引用堆积导致内存溢出)
            env->DeleteLocalRef(localRef);
        } else {
            LOGE("基于弱全局引用创建局部引用失败");
        }
    } else {
        // 弱全局引用已被回收,重新初始化
        LOGE("弱全局引用已被 GC 回收,开始重新初始化");
        reinitWeakGlobalRef(env);
    }
}

三、参考资料

JNI内存方面说明以及相关类型手动释放内存
Why I should not reuse a jclass and/or jmethodID in JNI?
Can I delete jmethodID and jFieldID safely?
NewGlobalRef for jmethodID


相关推荐
2501_944525543 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 预算详情页面
android·开发语言·前端·javascript·flutter·ecmascript
清蒸鳜鱼3 小时前
【Mobile Agent——Droidrun】MacOS+Android配置、使用指南
android·macos·mobileagent
2501_915918414 小时前
HTTPS 代理失效,启用双向认证(mTLS)的 iOS 应用网络怎么抓包调试
android·网络·ios·小程序·https·uni-app·iphone
峥嵘life4 小时前
Android EDLA CTS、GTS等各项测试命令汇总
android·学习·elasticsearch
Cobboo4 小时前
i单词上架鸿蒙应用市场之路:一次从 Android 到 HarmonyOS 的完整实战
android·华为·harmonyos
天下·第二4 小时前
达梦数据库适配
android·数据库·adb
定偶4 小时前
MySQL知识点
android·数据结构·数据库·mysql
iwanghang4 小时前
Android Studio 2023.2.1 新建项目 不能选择Java 解决方法
android·ide·android studio
南墙上的石头4 小时前
Android端 人工智能模型平台开发实战:模型服务部署与运维平台
android·运维