15

I have a JNI Callback:

void callback(Data *data, char *callbackName){
    JNIEnv *env;
    jvm->AttachCurrentThread((void **)&env, NULL);
    /* start useful code*/

    /* end useful code */
    jvm->DetachCurrentThread();
}

When I run it like this (empty useful code), I get a memory leak. If I comment out the whole method, there is no leak. What is the correct way of attaching / detaching threads?

My application processes real-time sound data, so the threads responsible for data processing must be done as soon as possible in order to be ready for another batch. So for these callbacks, I create new threads. There are dozens or even hundreds of them each second, they attach themselves to JVM, call a callback function which repaints a graph, detach and die. Is this a correct way of doing this stuff? How to handle the leaking memory?

EDIT: typo

OK I have created a mimimal code needed:

package test;

public class Start
{
    public static void main(String[] args) throws InterruptedException{
        System.loadLibrary("Debug/JNITest");
        start();
    }

    public static native void start();
}

and

#include <jni.h>
#include <Windows.h>
#include "test_Start.h"

JavaVM *jvm;
DWORD WINAPI attach(__in  LPVOID lpParameter);

JNIEXPORT void JNICALL Java_test_Start_start(JNIEnv *env, jclass){
    env->GetJavaVM(&jvm);
    while(true){
        CreateThread(NULL, 0, &(attach), NULL, 0, NULL);
        Sleep(10);
    }
}


DWORD WINAPI attach(__in  LPVOID lpParameter){
    JNIEnv *env;
    jvm->AttachCurrentThread((void **)&env, NULL);
    jvm->DetachCurrentThread();
    return 0;
}

and when I run the VisualJM profiler, I get the usual sawtooth pattern, no leak there. Heap usage peaked at around 5MB. However, observing the process explorer indeed shows some weird behaviour: memory is slowly rising and rising, 4K a second for a minute or so and then suddenly all this allocated memory drops. These drops do not correspond with garbage collection (they occur less often and deallocate less memory than those saw-teeth in the profiler).

So my best bet is that it is some OS behaviour handling tens of thousands milisecond-lived threads. Does some guru have an explanation for this?

Jakub Zaverka
  • 8,816
  • 3
  • 32
  • 48

2 Answers2

19

Several points about calling back into Java from native code:

  • AttachCurrentThread should only be called if jvm->GetEnv() returns JNI_EDETACHED. It's usually a no-op if the thread is already attached, but you can save some overhead.
  • DetachCurrentThread should only be called if you called AttachCurrentThread.
  • Avoid the detach if you expect to be called on the same thread in the future.

Depending on your native code's threading behavior, you may want to avoid the detach and instead store references to all native threads for disposal on termination (if you even need to do that; you may be able to rely on application shutdown to clean up).

If you continually attach and detach native threads, the VM must continually associate (often the same) threads with Java objects. Some VMs may re-use threads, or temporarily cache mappings to improve performance, but you'll get better and more predictable behavior if you don't rely on the VM to do it for you.

technomage
  • 9,861
  • 2
  • 26
  • 40
  • I see your point and I would normally agree. The threads I am creating are really short-lived. I create them (using WinApi CreateThread), then immediatelly attach them to the JVM. In Java, they repaint a Swing graph with a new value. When they are done, they detach and cease execution (return 0), at which point they should be destroyed by the OS. They are used to pass a new value to Java. There are about 40 of them each second for each sound channel (I work with up to 32 channels). – Jakub Zaverka Mar 10 '12 at 16:31
  • You might consider thread pooling. Make your threads stay around longer and take input from a queue, rather than continually spawning new threads. This will reduce thread management overhead in both the OS and Java. – technomage Mar 12 '12 at 15:23
  • 1
    If you're doing any Java stuff in the JNI apart from just calling a method, you might also want to push/pop a local frame around those actions. – technomage Mar 12 '12 at 15:24
  • Where are you detecting the leak? at the OS level? un-collected Java objects? native memory outside of java? – technomage Mar 12 '12 at 15:25
  • The memory requirement in process explorer climbs unbounded. If I omit the attach/detach, it stays constant. – Jakub Zaverka Mar 12 '12 at 15:41
  • Thread pooling sounds like a good idea, but is the extra code worth the cost? Is attach / detach really that expensive? – Jakub Zaverka Mar 12 '12 at 15:47
  • You should find the cause of your apparent leak first; if handled properly, attach/detach shouldn't result in a permanent increase in memory usage. Check to see if periodic calls to System.gc() result in any reclamation of the memory. – technomage Mar 12 '12 at 18:04
  • Are you sure about the 0 (JNI_OK) return from GetEnv? Surely you should call AttachCurrentThread ONLY if GetEnv returns JNI_EDETACHED (-2), as this signifies you're calling from a native thread created by pthread_create() which has no Java context. Calling it from a thread that's already attached (signified by the JNI_OK return from GetEnv) is a no-op, and calling DetachCurrentThread in that context is positively dangerous. – kbro Jun 09 '20 at 16:57
  • I think you are correct, I'm not sure why JNI_EDETACHED is not used. – technomage Jun 12 '20 at 14:30
9

I figured the problem. It was dangling local references in the JNI code I didn't destroy. Every callback would create a new local reference, thus resulting in a memory leak. When I converted the local reference to global, so I could reuse it, the problem disappeared.

Jakub Zaverka
  • 8,816
  • 3
  • 32
  • 48