前言
有过 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
并不是释放对象,只是在局部引用表中删除了局部引用而已