前言

有过 JNI 开发经历的都知道,在创建局部引用之后要释放。可是我们经常却直接返回 env->NewStringUTF("")Java 没有释放,并且不会有问题,本文就来研究一下局部引用的释放问题。

分析

为了研究 Java 的对象有没有被释放,我们需要自定义一个类,并且重写 finalize 这个方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class JNIRef
{
    static {
        System.loadLibrary("jref");
    }

    public static void main(String[] args)
    {
        JNIRef ref = new JNIRef();
        ref.ref();
    }

    public void ref() {
        RefObj refObj = NewRefObj();
        refObj.print();
    }

    public native RefObj NewRefObj();

    public static class RefObj {
        String name;
        RefObj innerObj;

        public RefObj(String name) {
            this.name = name;
        }

        public void print() {
            System.out.println(toString());
        }

        @Override
        public String toString() {
            return super.toString() + ", " + name + ", time: " + System.currentTimeMillis();
        }

        @Override
        protected void finalize() throws Throwable {
            System.out.println(this + " ," + name + " : destruct");
            super.finalize();
        }
    }
}

finalize 中我们打印了对象的地址和 name 这个字段以及 destruct 用来表示对象被释放了。

对应C++ 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <jni.h>
extern "C"
JNIEXPORT void JNICALL Java_JNIRef_NewRefObj(JNIEnv *env, jobject clazz) {
    jclass refObjClazz = env->FindClass("JNIRef$RefObj");
    jmethodID construct = env->GetMethodID(refObjClazz, "<init>", "(Ljava/lang/String;)V");
    jstring outter = env->NewStringUTF("RefObj From JNI");
    jobject refobj = env->NewObject(refObjClazz, construct, outter);
    jmethodID print = env->GetMethodID(refObjClazz, "print", "()V");
    env->CallVoidMethod(refobj, print);
}

执行看下结果

1
2
3
javac JNIRef.java
g++ -dynamiclib -fPIC -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin ref.cpp -o libjref.jnilib
java -Djava.library.path=./ JNIRef
1
JNIRef$RefObj@2a139a55 RefObj From JNI  time: 1656888892687

从输出来看,创建的 RefObj 并没有被释放,如果你对 JNI 比较熟悉应该是知道有 DeleteLocalRef 这个方法的,我们猜测一下是不是需要手动调用 DeleteLocalRef

DeleteLocalRef

修改 C++ 代码,加入 DeleteLocalRef

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <jni.h>
extern "C"
JNIEXPORT void JNICALL
Java_JNIRef_NewRefObj(JNIEnv *env, jobject clazz) {
    jclass refObjClazz = env->FindClass("JNIRef$RefObj");
    jmethodID construct = env->GetMethodID(refObjClazz, "<init>", "(Ljava/lang/String;)V");
    jstring outter = env->NewStringUTF("RefObj From JNI");
    jobject refobj = env->NewObject(refObjClazz, construct, outter);
    jmethodID print = env->GetMethodID(refObjClazz, "print", "()V");
    env->CallVoidMethod(refobj, print);
    // 手动释放
    env->DeleteLocalRef(refobj);
}

再次执行,结果如下

1
JNIRef$RefObj@2a139a55  RefObj From JNI  time: 1656889104009

很不幸 RefObj 依旧没有释放,这是怎么回事?难道是代码写错了?

GC

由于 Java 是带有垃圾回收的,所以我们不需要去释放内存,都由 GC 管理。

这里之所以没有释放很可能是没有触发 GC 所以导致没有释放 RefObj ,我们加入 GC 然后再试一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class JNIRef
{
    public static void main(String[] args)
    {
        JNIRef ref = new JNIRef();
        ref.NewRefObj();
        // 为了触发GC
        byte[] buffer = new byte[1024 * 1024 * 100];
        System.gc();
    }
    // ...
}

这里申请了 100M 的内存,目的是为了触发 GC ,执行之后结果如下

1
2
JNIRef$RefObj@2a139a55  RefObj From JNI  time: 1656889255901
JNIRef$RefObj@2a139a55  RefObj From JNI  time: 1656889255960 ,RefObj From JNI : destruct

果然释放了,如果我们把 DeleteLocalRef 删掉是不是也能回收 RefObj ,于是我把 DeleteLocalRef 删掉后再次执行,结果如下

1
2
JNIRef$RefObj@2a139a55  RefObj From JNI  time: 1656889569452
JNIRef$RefObj@2a139a55  RefObj From JNI  time: 1656889569512 ,RefObj From JNI : destruct

可以看到两次结果是一样的,有没有 DeleteLocalRef 并不影响对象的释放。

做过 JNI 的都知道, JNI 是有一个局部引用表的, JVM 要确保每个 Native 方法都可以创建 16 个局部引用,所以 DelteLocalRef 只是在局部引用表中删除引用而已,并不会释放内存。释放内存还是由 GC 处理。

既然我们知道 DeleteLocalRef 并不会释放内存,还要不要手动调用呢?

最佳实践是要调用,如果你不调用,一般情况下也不会有问题。因为在函数栈结束后,虚拟机会自动释放局部引用表,然后就会等 GC 触发之后回收。

那为什么又要调用呢?

因为局部引用表是有大小限制的,如果你创建过多的局部引用,超出限制了,再创建局部引用的时候就会导致程序退出。

所以用完之后就释放,这样就可以给其它局部引用挪出位置,不会导致程序退出。

不释放

上面讨论了局部引用的释放问题,如果我不想释放要怎么做呢?

根据上面的推理,我们的局部引用会受到 Java 虚拟机中的 GC 影响,所以从这个思路得出,我们只要持有这个对象的强引用 GC 就不会回收这个对象,写个代码验证一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class JNIRef
{
    static {
        System.loadLibrary("jref");
    }

    static RefObj obj;

    public static void main(String[] args)
    {
        // 保存 JNI 返回的引用
        obj = NewRefObj();
        byte[] buffer = new byte[1024 * 1024 * 100];
        System.gc();
    }

    public static native RefObj NewRefObj();

    public static class RefObj {
        String name;

        public RefObj(String name) {
            this.name = name;
        }

        public void print() {
            System.out.println(toString());
        }

        @Override
        public String toString() {
            return super.toString() + ", " + name + ", time: " + System.currentTimeMillis();
        }

        @Override
        protected void finalize() throws Throwable {
            System.out.println(this + " ," + name + " : destruct");
            super.finalize();
        }
    }
}

对应 C++ 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <jni.h>
extern "C"
JNIEXPORT jobject JNICALL Java_JNIRef_NewRefObj(JNIEnv *env, jclass clazz) {
    jclass refObjClazz = env->FindClass("JNIRef$RefObj");
    jmethodID construct = env->GetMethodID(refObjClazz, "<init>", "(Ljava/lang/String;)V");
    jstring outter = env->NewStringUTF("RefObj From JNI");
    jobject refobj = env->NewObject(refObjClazz, construct, outter);
    jmethodID print = env->GetMethodID(refObjClazz, "print", "()V");
    env->CallVoidMethod(refobj, print);
    // 返回 JNI 创建的对象
    return refobj;
}

执行后结果如下

1
JNIRef$RefObj@2a139a55  RefObj From JNI  time: 1656891033416

可以看到局部引用在触发 GC 之后并没有被释放,这也就是为什么我们经常写 env->NewStringUTF 直接返回到 Java 不会有问题的原因。

全局引用

除了返回局部引用到 Java 层,还有别的办法不释放吗?有,那就是全局引用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class JNIRef
{
    static {
        System.loadLibrary("jref");
    }

    public static void main(String[] args)
    {
        // 不保存 JNI 引用
        NewRefObj();
        byte[] buffer = new byte[1024 * 1024 * 100];
        System.gc();
    }

    public static native void NewRefObj();

    public static class RefObj {
        String name;

        public RefObj(String name) {
            this.name = name;
        }

        public void print() {
            System.out.println(toString());
        }

        @Override
        public String toString() {
            return super.toString() + ", " + name + ", time: " + System.currentTimeMillis();
        }

        @Override
        protected void finalize() throws Throwable {
            System.out.println(this + " ," + name + " : destruct");
            super.finalize();
        }
    }
}

对应 C++ 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <jni.h>
jobject obj;

extern "C"
JNIEXPORT void JNICALL Java_JNIRef_NewRefObj(JNIEnv *env, jclass clazz) {
    jclass refObjClazz = env->FindClass("JNIRef$RefObj");
    jmethodID construct = env->GetMethodID(refObjClazz, "<init>", "(Ljava/lang/String;)V");
    jstring outter = env->NewStringUTF("RefObj From JNI");
    jobject refobj = env->NewObject(refObjClazz, construct, outter);
    jmethodID print = env->GetMethodID(refObjClazz, "print", "()V");
    env->CallVoidMethod(refobj, print);
    obj = env->NewGlobalRef(refobj);
}

执行结果如下

1
JNIRef$RefObj@2a139a55  RefObj From JNI  time: 1656891534173

可以看到使用全局引用后,触发了 GC , refobj 也没有被释放掉

结论

  • JNI 中创建的 Java 对象受虚拟机 GC 的管控
  • 调用 DeleteLocalRef 并不是释放对象,只是在局部引用表中删除了局部引用而已