Why do I need to release global references created in JNI native functions?

10,861

Your code is failing because you have too many global references to objects that have not been released back into the memory pool. The global reference table has a maximum size to help you catch memory leaks and prevent your program from running out of memory. The log message you pasted tells you what those objects are: java.math.BigInteger.

If you look at your implementation of native_setup_bystring you can see that you're creating a global reference to a new BigInteger object:

jobject gjobj = env->NewGlobalRef(jobs);

Global references are not automatically garbage collected [1][2] so you need to explicitly delete them, which is what you've found in your testing.

The problem with your approach is that you're storing a reference to the internal object using a direct memory reference into the heap (your long mNativeContext field). This approach is not a good one because you're preventing the the JVM from managing the garbage collection of your BigIntegers. A better approach would be to avoid using a global reference at all, and store an object reference instead of a long. If you do this then the JVM will be able to automatically collect all of the objects you're allocating.

Share:
10,861
john liao
Author by

john liao

Updated on June 04, 2022

Comments

  • john liao
    john liao almost 2 years

    I have a Java class with native functions implemented in C++ which is called nz.ac.unitec.BigInteger. The native implementation of nz.ac.unitec.BigInteger is simple, just wraps the java.math.BigInteger and call its constructor and add, subtract, multiply ... functions. A field mNativeContext in nz.ac.unitec.BigInteger is used to store a global reference to a java.math.BigInteger object. The object has a finalizer that should destroy the global reference when the object is garbage collected so I am not leaking global references.

    When I runned a simple test loop to create a number of nz.ac.unitec.BigInteger objects without explicitly releasing the created objects, a JNI error was reported as following log (LOG1). But if I release the created objects explicitly before leaving the test function, the stress test can be executed successfully.

    Why is the JVM running out of global references when I have a finalizer to delete them when the objects are garbage collected?

    LOG1:

    F/art     (10730): art/runtime/indirect_reference_table.cc:113] JNI ERROR (app bug): global reference table overflow (max=51200)
    
    F/art     (10730): art/runtime/indirect_reference_table.cc:113] global reference table dump:
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]   Last 10 entries (of 51200):
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]     51199: 0x12e88790 java.math.BigInteger
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]     51198: 0x12e85490 java.math.BigInteger
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]     51197: 0x12e81790 java.math.BigInteger
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]     51196: 0x12e7e760 java.math.BigInteger
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]     51195: 0x12e7ab20 java.math.BigInteger
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]     51194: 0x12e77790 java.math.BigInteger
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]     51193: 0x12e73a90 java.math.BigInteger
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]     51192: 0x12e71af0 java.math.BigInteger
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]     51191: 0x12e6dd60 java.math.BigInteger
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]     51190: 0x12e6b9a0 java.math.BigInteger
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]   Summary:
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]      1456 of java.math.BigInteger (1456 unique instances)
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]         2 of android.opengl.EGLDisplay (2 unique instances)
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]      1889 of java.math.BigInteger (1889 unique instances)
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]         1 of java.lang.String
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]        27 of java.math.BigInteger (27 unique instances)
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]         1 of java.lang.String
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]      3771 of java.math.BigInteger (3771 unique instances)
    F/art     (10730): art/runtime/indirect_reference_table.cc:113]         1 of dalvik.system.PathClassLoader
    F/art     (10730): art/runtime/runtime.cc:284] Runtime aborting...
    F/art     (10730): art/runtime/runtime.cc:284] Aborting thread:
    

    Calculator.java

    package nz.ac.unitec.calculator;
    
    public class MainActivity extends AppCompatActivity
    {
        protected void onCreate(Bundle savedInstanceState)
        {
            Random ra = new Random();
            for(int i=0; i<6000000; ++i) {
                testCreateFinalize(ra);
                int m = ra.nextInt();
                int n = 8;
                int re = m + n;
                Log.i("MainActivity", "re=" + re);
                //BigInteger result = l.subtract(r);
            }
    
        private void testCreateFinalize(Random ra)
        {
            BigInteger l = new BigInteger("100", 10);
    
            BigInteger r = new BigInteger("200", 10);
            //l.release();      when adding this two code lines, the test is ok
            //r.release();;
        }
    }
    

    BigInteger.java

    package nz.ac.unitec.mathutils;
    public class BigInteger
    {
        static {
            System.loadLibrary("BigInteger_jni");
            native_init();
        }
    
        private long mNativeContext;
    
        private BigInteger()
        {
            mNativeContext = 0;
            native_setup();
        }
    
        public BigInteger(String val, int radix)
        {
            mNativeContext = 0;
            native_setup_bystring(val, radix);
        }
    
        public void release() {
            native_finalize();
        }
    
        protected void finalize() {
            native_finalize();
        }
    
    
        private static native final void native_init();
        private native final void native_setup();
        private native final void native_setup_bystring(String val, int radix);
        private native final void native_finalize();
        public native String  toString(int radix);
    
    
        public native BigInteger add(BigInteger rval);
        public native BigInteger multiply(BigInteger rval);
        public native BigInteger subtract(BigInteger rval);
    
    }
    

    Native Code

    static jobject getNativeBigInteger_l(JNIEnv* env, jobject thiz)
    {
        return reinterpret_cast<jobject> (env->GetLongField(thiz, fields.context));   //reinterpret_cast<jobject>
    }
    
    static void setNativeBigInteger_l(JNIEnv* env, jobject thiz, jobject bi)
    {
        env->SetLongField(thiz, fields.context, reinterpret_cast<jlong>(bi)); //reinterpret_cast<jlong>
    }
    
    JNIEXPORT void JNICALL Java_nz_ac_unitec_mathutils_BigInteger_native_1setup_1bystring
            (JNIEnv *env, jobject thiz, jstring val, jint radix)
    {
    
        jclass cls = env->FindClass(gBuiltinClassBigInteger);
        ALOGV("-Java_nz_ac_unitec_mathutils_BigInteger_native_1setup_1bystring env->FindClass(%s) return (%0x)",
              gBuiltinClassBigInteger, cls);
    
        if (cls == NULL) {
            ALOGE("-Java_nz_ac_unitec_mathutils_BigInteger_native_1setup_1bystring env->FindClass(%s) return NULL", gBuiltinClassBigInteger);
            return;
        }
    
        jmethodID constructor = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;I)V");
        if (constructor == NULL) {
            env->DeleteLocalRef(cls);
            ALOGE("-Java_nz_ac_unitec_mathutils_BigInteger_native_1setup_1bystring env->GetMethodID(%s) return NULL", "<init>");
            return;
        }
    
        jobject jobj = env->NewObject(cls, constructor, val, radix);
        ALOGV("-Java_nz_ac_unitec_mathutils_BigInteger_native_1setup_1bystring env->NewObject return (%0x)",
              jobj);
        if (NULL == jobj) {
            env->DeleteLocalRef(cls);
            ALOGE("-Java_nz_ac_unitec_mathutils_BigInteger_native_1setup_1bystring env->NewObject return NULL");
            return;
        }
    
        jobject gjobj = env->NewGlobalRef(jobj);
        ALOGV("-Java_nz_ac_unitec_mathutils_BigInteger_native_1setup_1bystring env->NewGlobalRef return (%0x)", gjobj);
        setNativeBigInteger_l(env, thiz, gjobj);
        env->DeleteLocalRef(jobj);
        //env->DeleteLocalRef(cls);
    }
    
    JNIEXPORT void JNICALL
    Java_nz_ac_unitec_mathutils_BigInteger_native_1finalize
            (JNIEnv *env, jobject thiz)
    {
        ALOGV("+native_finalize");
        jobject obj = getNativeBigInteger_l(env, thiz);
        ALOGV("-Java_nz_ac_unitec_mathutils_BigInteger_native_1finalize getNativeBigInteger_l return (%0x)",
                              obj);
        if (obj == NULL) {
            ALOGE("-native_finalize getNativeBigInteger_l NULL");
            return;
        }
        env->DeleteGlobalRef(obj);
        setNativeBigInteger_l(env, thiz, NULL);
        ALOGV("-native_finalize");
    }
    
    
    static JNINativeMethod gMethods[] = {
            {"native_init",         "()V",
                    (void *)Java_nz_ac_unitec_mathutils_BigInteger_native_1init},
    
            {"native_setup",    "()V",
                    (void *)Java_nz_ac_unitec_mathutils_BigInteger_native_1setup},
    
            {"native_setup_bystring", "(Ljava/lang/String;I)V",
                    (void*)Java_nz_ac_unitec_mathutils_BigInteger_native_1setup_1bystring},
    
    
            {"native_finalize",     "()V",
                    (void *)Java_nz_ac_unitec_mathutils_BigInteger_native_1finalize},
    
            {"add",     "(Lnz/ac/unitec/mathutils/BigInteger;)Lnz/ac/unitec/mathutils/BigInteger;",
             (void *)Java_nz_ac_unitec_mathutils_BigInteger_add},
    
            {"multiply","(Lnz/ac/unitec/mathutils/BigInteger;)Lnz/ac/unitec/mathutils/BigInteger;",
                    (void *)Java_nz_ac_unitec_mathutils_BigInteger_multiply},
            {"subtract","(Lnz/ac/unitec/mathutils/BigInteger;)Lnz/ac/unitec/mathutils/BigInteger;",
                    (void *)Java_nz_ac_unitec_mathutils_BigInteger_subtract},
            {"toString","(I)Ljava/lang/String;",
                    (void *)Java_nz_ac_unitec_mathutils_BigInteger_toString}
    };
    
    int register_Java_nz_ac_unitec_mathutils_BigInteger(JNIEnv *env)
    {
        return jniRegisterNativeMethods(env, gClassBigInteger, gMethods, NELEM(gMethods));
    }
    
  • john liao
    john liao about 8 years
    The thing is that nz.ac.mathutils.BigInteger.finalize() will be called periodically because testCreateFinalize will return periodically, all the local references created in testCreateFinalize will be released automatically. As the local reference (nz.ac.mathutils.BigInteger) is being released, the nz.ac.mathutils.BigInteger.finalize function will be called automatically. Then in the native_finalize JNI function, the global reference will be released through env->DeleteGlobalRef(obj);. This kind of implementation is just like the JNI object android.media.MediaScanner in AOSP.
  • Stephen Nelson
    Stephen Nelson about 8 years
    While that's true, garbage collection will not be triggered by global reference exhaustion so there's no way for the JVM to reclaim this resource intentionally, which is why you're seeing this error. Global references are not intended to be used in this way. MediaScanner can use this strategy because it's already tied to a scarce OS resource (file handles) that ensure it won't be allocated too often. This is not a good strategy when you have a JVM object you can refer to.
  • john liao
    john liao about 8 years
    the DeleteGlobalRef will be called in the context of "finalize" function of the local reference of nz.ac.mathutils.BigInteger class, so I think the JVM global resource table will never reach the limit. So, my question is why it reached the limit.
  • Stephen Nelson
    Stephen Nelson about 8 years
    Your code creates an object, which creates a global ref, then throws away the reference to it. At some point the future, the JVM will run the finalise method that will cause the global reference to be destroyed. You are assuming this will happen before the global resource table is exhausted. Your assumption is wrong, the JVM is not running GC yet because it still has lots of free memory, but it's hitting the global resource table limit. You could verify this by adding a System.gc() call after each step in your allocation loop.
  • Weekend
    Weekend almost 6 years
    Oh no, the link is broken now.
  • Stephen Nelson
    Stephen Nelson over 5 years
    Fixed the broken link