java-native-access/jna · error

JNA: callback object has been garbage collected

Error message

JNA: callback object has been garbage collected

What it means

During callback dispatch, JNA creates a NewLocalRef of the stored callback object and checks IsSameObject against NULL — the standard test for whether the underlying object was garbage collected (the stored reference is weak). If collected, JNA prints this message, zeroes the return buffer, and skips the Java method invocation: the native caller gets a zeroed/void result and the callback silently does nothing.

Source

Thrown at native/dispatch.c:2078

  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) {
      fprintf(stderr, "JNA: could not detach thread after callback handling\n");
    }
  }
}

View on GitHub (pinned to d036ad9781)

Solutions

  1. Keep a strong reference to every Callback instance you register (a field, static map, or list) for as long as the native side can invoke it.
  2. Provide an explicit unregister/dispose API call that removes the native registration before dropping the Java reference.
  3. If using a lambda or anonymous class, assign it to a long-lived variable rather than passing a fresh instance inline.
  4. On hitting the error, identify which callback was collected and re-register it while holding the reference.

Example fix

// before: callback only weakly held by native side
lib.register_callback(new MyCallback());
// after: strong Java-side reference
private static final MyCallback CB = new MyCallback();
void init() { lib.register_callback(CB); }
void shutdown() { lib.unregister_callback(CB); }
Defensive patterns

Strategy: validation

Validate before calling

// Keep a strong reference registry for callbacks passed to native code:
private static final Set<Callback> LIVE_CALLBACKS = ConcurrentHashMap.newKeySet();
static void registerGuarded(Library lib, Callback cb) {
  LIVE_CALLBACKS.add(cb);
  lib.register_callback(cb);
}

Type guard

static boolean isCallbackAlive(Callback cb) {
  return cb != null && LIVE_CALLBACKS.contains(cb);
}

Prevention

When it happens

Trigger: The Java object implementing the Callback was only weakly reachable (e.g. the caller stored the callback only in native code / passed it to the native library without keeping a strong Java reference), and the GC collected it before the native side invoked it.

Common situations: Passing an anonymous callback lambda to nativeLib.register(...) without storing it in a field; callback used only from C-side event tables; long-lived native subscriptions registered with short-lived Java wrappers.

Related errors


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