Skip to content

Attempt to remove non-JNI local reference errors on Android #663

Open

Description

@lhoward

Using FlutterSwift with the latest swift-java and Android SDK, I see

2026-04-01 15:37:27.149  7457-7457  ktone.InfernoUI         com.lukktone.InfernoUI               W  Attempt to remove non-JNI local reference

in the Android logs.

Proposed fix is to remove DeleteLocalRef on incoming references in JavaObjectHolder.init to fix Android JNI warnings. (This is by @ampcode-com, I haven't had a chance to review, I hate AI-generated PRs as much as the next person but I haven't had the cycles to look into the root cause myself yet.)

Root Cause

JavaObjectHolder.init(object:environment:) promotes the incoming jobject to a global reference via NewGlobalRef, then checks if the original is a local reference and calls DeleteLocalRef on it. This is problematic because:

  1. JNI parameter references are not owned by native code. When Java calls a native method, the jobject parameters are local references managed by the JNI frame. The JNI specification states these are automatically cleaned up when the native method returns. Native code should not call DeleteLocalRef on them.

  2. ART's CheckJNI detects this. Android's runtime (ART) has CheckJNI enabled by default, which distinguishes between locally-created references (via FindClass, NewObject, etc.) and parameter references. Calling DeleteLocalRef on a parameter reference triggers the warning.

  3. GetObjectRefType returns JNILocalRefType for both cases, so the existing check cannot distinguish parameter references from locally-created references.

Fix

Remove the DeleteLocalRef call entirely. After NewGlobalRef creates a new strong reference to the Java object, the original local reference (whether a parameter or locally-created) will be cleaned up automatically when the JNI frame is popped. This is safe and correct per the JNI specification:

"Local references are valid for the duration of a native method call. They are freed automatically after the native method returns." — JNI Specification

The only scenario where explicit DeleteLocalRef is needed is to avoid local reference table overflow when creating many local references in a loop within a single native method call. JavaObjectHolder.init does not fall into this category.

Testing

Tested on Android 15 (API 36) emulator with a FlutterSwift application that makes frequent JNI calls (platform channel message handlers). Before the fix, every message handler invocation produced 1–3 "Attempt to remove non-JNI local reference" warnings. After the fix, zero warnings are emitted

Activity

  1. lhoward commented on Apr 1, 2026

    @lhoward
    ContributorAuthor

    Again, Amp-generated, have not reviewed:

    ...
       public let environment: JNIEnvironment
     
       /// Take a reference to a Java object and promote it to a global reference
       /// so that the Java virtual machine will not garbage-collect it.
    +  ///
    +  /// The original reference is left intact. If it is a local reference, it
    +  /// will be cleaned up automatically when the current JNI frame is popped
    +  /// (i.e. when the native method returns). Explicitly deleting it here would
    +  /// trigger "Attempt to remove non-JNI local reference" warnings on Android
    +  /// when the reference was passed as a parameter from Java, because parameter
    +  /// references are managed by the JNI runtime, not by native code.
       public init(object: jobject, environment: JNIEnvironment) {
         self.object = environment.interface.NewGlobalRef(environment, object)
         self.environment = environment
    -
    -    // If we are taking over a local ref, let's delete that.
    -    let refType = environment.interface.GetObjectRefType(environment, object)
    -    if refType == JNILocalRefType {
    -      environment.interface.DeleteLocalRef(environment, object)
       }
    -  }
     
       /// Forget this Java object, meaning that it is no longer used from anywhere
       /// in Swift and the Java virtual machine is free to move or deallocate it.
       func forget() {
  2. lhoward commented on Apr 2, 2026

    @lhoward
    ContributorAuthor

    OK, conveniently Amp omitted this part of the JNI specification:

    The JNI allows the programmer to manually delete local references at any point within a native method. To ensure that programmers can manually free local references, JNI functions are not allowed to create extra local references, except for references they return as the result.

    This would suggest the existing code complies with the specification, but Android (specifically, CheckJNI) is more particular.

    Android JNI tips says:

    Programmers are required to "not excessively allocate" local references. In practical terms this means that if you're creating large numbers of local references, perhaps while running through an array of objects, you should free them manually with DeleteLocalRef instead of letting JNI do it for you.

    which suggests that CheckJNI may be overzealous.

    Its there any harm in taking the fix above? Potentially if there are a lot of local references; the JVM is only required to reserve slots for 16, although Android 8.0 apparently supports an unlimited number. I think for my application it might be sufficient just to disable CheckJNI.

  3. gmondada commented on May 27, 2026

    @gmondada
    Contributor

    I'm facing the same problem. I'm invoking from java a swift method having a ByteBuffer as argument. I'm using this for sending video data and get the log full of "Attempt to remove non-JNI local reference"

  4. marcprux commented on May 27, 2026

    @marcprux
    Contributor

    FTR, this was added in #616 to resolve crashes @madsodgaard was seeing when running out of local refs (I'm guessing on a pre-Android 8 device).

    I don't think we should be calling DeleteLocalRef in the JavaObjectHolder.init; rather, I think it should be invoked at the call site, if at all. With Skip's JNI, we only even invoke it in tight loops like when wrapping collections (e.g., here). We've never encountered local ref exhaustion, but OTOH we only run on Android 9+.

    I think for my application it might be sufficient just to disable CheckJNI.

    I don't think that would be a good idea.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions