-
Notifications
You must be signed in to change notification settings - Fork 42
Description
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:
- For the class name lookup — executed for every
to_rust()call regardless of the target type - 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 summaryThe 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().