Skip to content

Native memory leak in string_from_jobject — missing call to get_jni_release_string_utf_chars #161

@ThomasKiec

Description

@ThomasKiec

Environment

  • j4rs Rust crate: 0.25.0
  • j4rs Java artifact (io.github.astonbitecode:j4rs): 0.25.0
  • j4rs_derive: 0.1.1

Description

The function jni_utils::string_from_jobject in rust/src/jni_utils.rs calls cache::get_jni_get_string_utf_chars() to obtain a native string buffer, but never calls cache::get_jni_release_string_utf_chars() to free it. This causes a native memory leak on every invocation.

The correct counterpart jni_utils::jstring_to_rust_string (in the same file, a few lines below) does call cache::get_jni_release_string_utf_chars() — so this appears to be an oversight.

string_from_jobject is called from Jvm::to_rust_boxed (and transitively from Jvm::to_rust) in two places:

  1. For the class name lookup — executed for every to_rust() call regardless of the target type
  2. For the actual String value extraction — executed additionally when the target type is String

This means every to_rust() call leaks at least one native string buffer. When converting a String, it leaks two.

In long-running applications with frequent to_rust() calls, this leads to unbounded growth of native memory (visible in the JVM's Internal category via -XX:NativeMemoryTracking=summary).

How to Reproduce

use j4rs::prelude::*;
use j4rs::InvocationArg;

fn main() {
    let jvm = Jvm::new(&[], None).unwrap();

    for _ in 0..1_000_000 {
        let instance = jvm.create_instance(
            "java.lang.String",
            &[InvocationArg::try_from("test").unwrap()],
        ).unwrap();
        let _: String = jvm.to_rust(instance).unwrap();
    }
}

Run with NMT enabled:

JAVA_TOOL_OPTIONS="-XX:NativeMemoryTracking=summary" cargo run
# In another terminal:
jcmd <pid> VM.native_memory summary

The Internal section will show continuous, unbounded growth.

Suggested Fix

Add the missing cache::get_jni_release_string_utf_chars() call in string_from_jobject after copying the data into the Rust String (which is safe because utils::to_rust_string copies the bytes via CStr::from_ptr):

 pub(crate) unsafe fn string_from_jobject(
     obj: jobject,
     jni_env: *mut JNIEnv,
 ) -> errors::Result<String> {
     if obj.is_null() {
         Err(...)
     } else {
         let s = (opt_to_res(cache::get_jni_get_string_utf_chars())?)(jni_env, obj, ptr::null_mut())
             as *mut c_char;
         let rust_string = utils::to_rust_string(s)?;
+        (opt_to_res(cache::get_jni_release_string_utf_chars())?)(jni_env, obj, s as *const c_char);
         Ok(rust_string)
     }
 }

Workaround

Use jvm.to_rust_deserialized(instance) instead of jvm.to_rust(instance). The deserialized path uses jni_utils::jstring_to_rust_string internally, which correctly calls cache::get_jni_release_string_utf_chars().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions