Java JNI Optimization Guide: Improve Native Code Performance with Examples

We’ll walk through what JNI (Java Native Interface) is, why it’s both powerful and tricky, and how to use it efficiently. Along the way, I’ll show you some bits of code examples to demonstrate both bad and optimized JNI usage.

1. What is JNI?

JNI (Java Native Interface) is the bridge between Java and native code (usually C or C++).

It allows Java applications to call into high-performance native libraries, GPU APIs, or OS-level functions that aren’t directly accessible from Java.

Rule of thumb: don’t overuse JNI. Use it only where performance really matters.


2. Five Key Ideas for Optimizing JNI Calls

1. Reduce the number of JNI calls

public class JNIDemo {
    public static native int square(int x); // bad examples

    public static native int sumOfSquares(int[] arr); // good examples
}

2. Avoid unnecessary data copying

public class JNIDemoBuffer {
    public static native long sumOfSquaresBuffer(ByteBuffer buf, int len);

    public static void main(String[] args) {

        // Allocate off-heap memory (not in Java heap)
        ByteBuffer buf = ByteBuffer.allocateDirect(n * 4);
        for (int i = 0; i < n; i++) buf.putInt(i);
        buf.flip();

        long sum = sumOfSquaresBuffer(buf, n);
    }
}

3. Optimize string handling

JNIEXPORT void JNICALL Java_JNIDemoString_printSubstring
  (JNIEnv *env, jclass cls, jstring input, jint start, jint len) {
    char buf[64];
    // Directly copy the substring [start, start+len) into buf
    (*env)->GetStringUTFRegion(env, input, start, len, buf);

    buf[len] = '\0'; // Null-terminate the string
}

4. Minimize temporary object allocations

5. Cache and static binding

public class JNIDemoCache {
    // Use static native method to avoid virtual dispatch
    public static native void callCached();

    public void sayHello() {
        System.out.println("Hello from Java instance method!");
    }
}

// Global variables to cache methodID and class reference
static jmethodID mid_sayHello = NULL;
static jclass cls_cache = NULL;

JNIEXPORT void JNICALL Java_JNIDemoCache_callCached
  (JNIEnv *env, jclass cls) {
    // Allocate an object without calling its constructor
    jobject obj = (*env)->AllocObject(env, cls_cache);

    // Call the cached Java method "sayHello"
    (*env)->CallVoidMethod(env, obj, mid_sayHello);
}

// Cache methodID and class reference when the library is loaded
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    (*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_8);

    // Find the Java class and create a global reference
    jclass localCls = (*env)->FindClass(env, "JNIDemoCache");
    cls_cache = (*env)->NewGlobalRef(env, localCls);

    // Cache the methodID of "sayHello"
    mid_sayHello = (*env)->GetMethodID(env, cls_cache, "sayHello", "()V");

    // Return the JNI version
    return JNI_VERSION_1_8;
}

3. What’s Next?

JNI is powerful but tricky. Modern JVMs are introducing safer alternatives.

These new APIs aim to replace JNI with a safer, more ergonomic, and more performant way to call native code.