java-native-access/jna · error

JNA: Out of memory: Can't allocate local frame

Error message

JNA: Out of memory: Can't allocate local frame

What it means

Before dispatching a native callback invocation, JNA pushes a JNI local frame (PushLocalFrame, capacity 16) so all local references created during the callback are disposed at once. If PushLocalFrame fails (native out of memory), JNA prints this message to stderr and skips the normal dispatch path — the Java callback is not invoked and the native response buffer handling is bypassed.

Source

Thrown at native/dispatch.c:2073

closure_handler(ffi_cif* cif, void* resp, void** argp, void *cdata)
{
  callback* cb = (callback *)cdata;
  JavaVM* jvm = cb->vm;
  JNIEnv* env;
  jobject obj;
  int attached = (*jvm)->GetEnv(jvm, (void *)&env, JNI_VERSION_1_4) == JNI_OK;

  if (!attached) {
    if ((*jvm)->AttachCurrentThread(jvm, (void *)&env, NULL) != JNI_OK) {
      fprintf(stderr, "JNA: Can't attach native thread to VM for closure handler\n");
      return;
    }
  }

  // Give the callback its own local frame to ensure all local references
  // are properly disposed
  if ((*env)->PushLocalFrame(env, 16) < 0) {
    fprintf(stderr, "JNA: Out of memory: Can't allocate local frame");
  }
  else {
    obj = (*env)->NewLocalRef(env, cb->object);
    if ((*env)->IsSameObject(env, obj, NULL)) {
      fprintf(stderr, "JNA: callback object has been garbage collected\n");
      if (cif->rtype->type != FFI_TYPE_VOID)
        memset(resp, 0, cif->rtype->size);
    }
    else {
      (*env)->CallVoidMethod(env, obj, MID_ffi_callback_invoke,
                             A2L(cif), A2L(resp), A2L(argp));
    }

    (*env)->PopLocalFrame(env, NULL);
  }

  if (!attached) {
    if ((*jvm)->DetachCurrentThread(jvm) != 0) {

View on GitHub (pinned to d036ad9781)

Solutions

  1. Free native memory / increase available RAM or native heap for the process (e.g. container memory limits, -Xmx interplay with native allocations).
  2. Reduce depth/frequency of nested native-to-Java callback chains that hold local frames.
  3. Profile with JNI local reference tracking (-verbose:jni, -Xcheck:jni) to find leaks keeping frames alive.
  4. Add out-of-memory monitoring and graceful degradation so callbacks are unregistered before memory pressure becomes fatal.

Example fix

// before: unbounded native thread pool each attaching to JVM
for (int i = 0; i < 100000; i++) new Thread(nativeCallbackTask).start();
// after: bounded pool
ExecutorService pool = Executors.newFixedThreadPool(16);
for (Task t : tasks) pool.submit(nativeCallbackTask);
Defensive patterns

Strategy: fallback

Validate before calling

// Check headroom before registering many callback threads:
long freeNative = ((com.sun.jna.Pointer) null) != null ? Runtime.getRuntime().freeMemory() : 0;
if (Runtime.getRuntime().freeMemory() < 64L * 1024 * 1024) {
  throw new IllegalStateException("Insufficient memory headroom for callback registration");
}

Prevention

When it happens

Trigger: A JNA Callback fires while the JVM/native heap is exhausted, so (*env)->PushLocalFrame(env, 16) returns a negative value.

Common situations: Native memory exhaustion in long-running services with many threads attached via JNI; JNI local-reference pressure in deeply recursive native-to-Java callback chains; containers with undersized RAM limits.

Related errors


AI-assisted analysis of java-native-access/jna@d036ad9781 (2026-09-12). Data as JSON: /api/errors/b9567a25ea615211. Report an issue: GitHub.