Reputation: 24089
The JNI docs describe the rules for resource management of objects returned from the JNI. Here's a quote that gives a good overview:
The JNI divides object references used by the native code into two categories: local and global references. Local references are valid for the duration of a native method call, and are automatically freed after the native method returns. Global references remain valid until they are explicitly freed.
Objects are passed to native methods as local references. All Java objects returned by JNI functions are local references. The JNI allows the programmer to create global references from local references. JNI functions that expect Java objects accept both global and local references. A native method may return a local or global reference to the VM as its result.
This documentation is obviously geared towards using the JNI to implement methods in native code but the JNI can also be used for embedding. What are the rules for embedding? When the 'native method call' that a JNI function returns to is an enclosing program which is embedding a VM, the 'native method call', i.e. the program, will never return to the VM. What are the rules in that case? Can a program that embeds the JNI tell the JNI that it can free a previously returned object?
Edit:
Here's an example of code where I'm not sure how to handle an object returned from the JNI based on the docs. TestKlass.java
defines a simple Java class. run.c
starts an embedded Java VM and loads the TestKlass
class using the JNI and then runs the TestKlass
constructor to get a jobject
instance of TestKlass
. What are the resource management rules for the returned jobject
? When will the Java VM assume that it can safely release the object?
The code starts the Java VM with Xcheck:jni
and the VM does not print any errors, but that doesn't guarantee that no errors exist in this code. If there are errors in this example, how could I have detected them?
TestKlass.java
public class TestKlass {
public TestKlass() {
System.out.println("Java: TestKlass::TestKlass()");
}
}
run.c
#include <jni.h>
#include <stdlib.h>
JNIEnv* create_vm(JavaVM** jvm)
{
JNIEnv* env;
JavaVMInitArgs vm_args;
// For this example, TestKlass.java and run.c are assumed to live in and
// be compiled in the same directory, so '.' is added to the Java
// path.
char opts0[] = "-Djava.class.path=.";
char opts1[] = "-Xcheck:jni";
JavaVMOption opts[2];
opts[0].optionString = opts0;
opts[1].optionString = opts1;
vm_args.version = JNI_VERSION_1_6;
vm_args.nOptions = 2;
vm_args.options = opts;
vm_args.ignoreUnrecognized = 0;
jint r = JNI_CreateJavaVM(jvm, (void**)&env, &vm_args);
if (r < 0 || !env) {
printf("Unable to Launch JVM %d\n", r);
abort();
}
printf("Launched JVM! :)\n");
return env;
}
int main(int argc, char **argv)
{
JavaVM *jvm;
JNIEnv *env;
env = create_vm(&jvm);
if(env == NULL)
return 1;
jclass cls = (*env)->FindClass(env, "TestKlass");
jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
jobject jobj = (*env)->NewObject(env, cls, mid);
// (Assume that arbitrary JNI calls will be made after this point, the
// program will persist for a long time, and returned Java objects may
// be large.)
// What are the rules for negotiating management of jobj with the Java VM?
// Does the answer change if the object was returned from a
// non-constructor function?
// Is there any way to use the JNI to tell the Java VM either that jobj
// can be freed here or that it must not be freed here?
// Would DeleteLocalRef() be helpful here?
// ...
(*jvm)->DestroyJavaVM(jvm);
}
output
Launched JVM! :)
Java: TestKlass::TestKlass()
I know C but I haven't used Java in a long time. I'm mostly working from the JNI docs. If I'm missing something obvious, please point it out in an answer or comment.
Upvotes: 4
Views: 3481
Reputation: 2910
I did some investigation on this while writing a Java plugin for Unity. I didn't find the JVM releasing any local references without a call to either deleteLocalRef
or popLocalFrame
, making them effectively global references, although they probably shouldn't be used that way.
Here's my test, in which I manually checked the printed reference table outputs from Logcat on a Pixel 5.
using System;
using NUnit.Framework;
using UnityEngine;
/// <summary>
/// Tests various JNI behaviors.
/// </summary>
public class JniTest
{
[Test]
public void TestLocalReference()
{
CreateLocalReference(10000000);
DumpReferenceTables();
}
[Test]
public void TestGlobalReference()
{
CreateGlobalReference(10000000);
DumpReferenceTables();
}
private void CreateLocalReference(int count)
{
for (int i = 0; i < count; i++)
{
AndroidJNI.NewString(i.ToString());
}
}
private void CreateGlobalReference(int count)
{
for (int i = 0; i < count; i++)
{
IntPtr local = AndroidJNI.NewString(i.ToString());
AndroidJNI.NewGlobalRef(local);
AndroidJNI.DeleteLocalRef(local);
}
}
private void DumpReferenceTables()
{
IntPtr debugClass = AndroidJNI.FindClass("android/os/Debug");
IntPtr methodId = AndroidJNI.GetStaticMethodID(debugClass, "dumpReferenceTables", "()V");
AndroidJNI.CallStaticVoidMethod(debugClass, methodId, null);
}
}
Interestingly, Unity on Android has a different max count than the usually advertised 512 local and 65536 global. I get the following crash logs:
JNI ERROR (app bug): global reference table overflow (max=51200)global reference table dump:
runtime.cc:677] Last 10 entries (of 51200):
runtime.cc:677] 51199: 0x12fc8f10 java.lang.String "50695"
runtime.cc:677] 51198: 0x12fc8ef8 java.lang.String "50694"
runtime.cc:677] 51197: 0x12fc8ee0 java.lang.String "50693"
runtime.cc:677] 51196: 0x12fc8ec8 java.lang.String "50692"
runtime.cc:677] 51195: 0x12fc8eb0 java.lang.String "50691"
runtime.cc:677] 51194: 0x12fc8e98 java.lang.String "50690"
runtime.cc:677] 51193: 0x12fc8e80 java.lang.String "50689"
runtime.cc:677] 51192: 0x12fc8e68 java.lang.String "50688"
runtime.cc:677] 51191: 0x12fc8e50 java.lang.String "50687"
runtime.cc:677] 51190: 0x12fc8e38 java.lang.String "50686"
...
And a similar yet surprising stack for local references:
JNI ERROR (app bug): local reference table overflow (max=8388608)
...
The limits might be different for other phones/setups, but the lesson is to always clean up local references when running stuff from native code.
I also tested the performance tradeoff of using delete vs pop, and found delete faster when cleaning up 2+ refs. Here's the results of running 1 million times of creating then deleting.
Refs/call | Push/Pop | DeleteLocalRef | Diff
0 474 ms 1078 ms 56%
1 5924 ms 6590 ms 10%
2 14986 ms 10716 ms -40%
3 22581 ms 14618 ms -54%
4 30045 ms 18418 ms -63%
Here's the test's source code:
using System;
using NUnit.Framework;
using UnityEngine;
public class JniTest
{
[Test]
public void TestPerformance()
{
var watch = new System.Diagnostics.Stopwatch();
int iterations = 1000000;
for (int i = 0; i < 5; i++)
{
watch.Start();
MethodCallFrame(i, iterations);
watch.Stop();
long frameMs = watch.ElapsedMilliseconds;
watch.Restart();
MethodCallNoFrame(i, iterations);
watch.Stop();
long noFrameMs = watch.ElapsedMilliseconds;
Debug.Log($"Test: {frameMs}, {noFrameMs}, {1 - (double)frameMs / noFrameMs}");
}
}
private void MethodCallFrame(int args, int iterations)
{
for (int i = 0; i < iterations; i++)
{
AndroidJNI.PushLocalFrame(0);
for (int j = 0; j < args; j++)
{
AndroidJNI.NewString(j.ToString());
}
AndroidJNI.PopLocalFrame(IntPtr.Zero);
}
}
private void MethodCallNoFrame(int argCount, int iterations)
{
for (int i = 0; i < iterations; i++)
{
IntPtr[] args = new IntPtr[argCount];
for (int j = 0; j < argCount; j++)
{
args[j] = AndroidJNI.NewString(j.ToString());
}
foreach (IntPtr ptr in args)
{
AndroidJNI.DeleteLocalRef(ptr);
}
}
}
}
Upvotes: 1
Reputation: 20812
As you say, you are not in a native
method so there will be no cleaning up upon "return". Your question is about cleaning well before return.
To free a local reference, you have two choices:
DeleteLocalRef
for one reference.PushLocalFrame
/PopLocalFrame
for a group of references.(I suspect PushLocalFrame
/PopLocalFrame
is how the clean up of a native
method is done.)
Example:
TestKlass.java
public class TestKlass {
public TestKlass() {
System.out.println("Java: TestKlass::TestKlass()");
}
public void finalize() {
System.out.println("Java: TestKlass::finalize()");
}
public static void force_gc() {
System.out.println("Java: TestKlass::force_gc()");
System.gc();
System.runFinalization();
}
}
run.c
#include <jni.h>
#include <stdlib.h>
JNIEnv* create_vm(JavaVM** jvm)
{
JNIEnv* env;
JavaVMInitArgs vm_args;
// For this example, TestKlass.java and run.c are assumed to live in and
// be compiled the same directory, so '.' is added to the Java path.
char opts0[] = "-Djava.class.path=.";
char opts1[] = "-Xcheck:jni";
JavaVMOption opts[2];
opts[0].optionString = opts0;
opts[1].optionString = opts1;
vm_args.version = JNI_VERSION_1_6;
vm_args.nOptions = 2;
vm_args.options = opts;
vm_args.ignoreUnrecognized = 0;
jint r = JNI_CreateJavaVM(jvm, (void**)&env, &vm_args);
if (r < 0 || !env) {
printf("Unable to Launch JVM %d\n", r);
abort();
}
printf("Launched JVM! :)\n");
return env;
}
int main(int argc, char **argv)
{
JavaVM *jvm;
JNIEnv *env;
env = create_vm(&jvm);
if(env == NULL)
return 1;
jclass cls = (*env)->FindClass(env, "TestKlass");
jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
jobject jobj = (*env)->NewObject(env, cls, mid);
(*env)->DeleteLocalRef(env, jobj);
jmethodID mid2 = (*env)->GetStaticMethodID(env, cls, "force_gc", "()V");
(*env)->CallStaticVoidMethod(env, cls, mid2);
(*jvm)->DestroyJavaVM(jvm);
}
output
Launched JVM! :)
Java: TestKlass::TestKlass()
Java: TestKlass::force_gc()
Java: TestKlass::finalize()
Removing either the call to DeleteLocalRef()
or the call to force_gc()
prevents finalize()
from running.
Upvotes: 4
Reputation: 310980
You must turn your jobj
into a GlobalRef
as soon as you acquire it, and call DeleteGlobalRef()
when you're finished with it.
Upvotes: 0