From a2e4b4e71fc70181643c4f62253251742b4bcb72 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sun, 25 Oct 2026 08:38:25 +0900 Subject: [PATCH 1/3] Improve native GC lifetimes and member resolution --- jvm/src/external_roots.rs | 9 + jvm/src/garbage_collector.rs | 73 ++++--- jvm/src/jvm.rs | 150 ++++++++++---- jvm/src/lib.rs | 4 +- jvm/src/monitor.rs | 18 +- jvm/tests/test_garbage_collection.rs | 187 +++++++++++++++++- jvm/tests/test_string.rs | 2 +- .../src/classes/java/lang/runtime.rs | 8 +- .../src/classes/java/lang/system.rs | 8 +- .../tests/classes/java/lang/test_class.rs | 2 +- src/lib.rs | 2 +- .../MonitorSemantics$CounterWorker.class | Bin 970 -> 1014 bytes .../MonitorSemantics$FailingWorker.class | Bin 732 -> 732 bytes test-data/MonitorSemantics.class | Bin 1720 -> 1742 bytes test-data/ThreadInterruption.class | Bin 3541 -> 3563 bytes test-data/src/MonitorSemantics.java | 2 + test-data/src/ThreadInterruption.java | 1 + test-utils/src/lib.rs | 6 +- 18 files changed, 384 insertions(+), 88 deletions(-) create mode 100644 jvm/src/external_roots.rs diff --git a/jvm/src/external_roots.rs b/jvm/src/external_roots.rs new file mode 100644 index 00000000..4d634fab --- /dev/null +++ b/jvm/src/external_roots.rs @@ -0,0 +1,9 @@ +use alloc::vec::Vec; + +use crate::ClassInstance; + +/// Discovers roots in a consumer's execution state during synchronous collection. +/// Implementations must not call JVM APIs, run guest code, or mutate the object inventory. +pub trait ExternalRoots: Send + Sync { + fn roots(&self, objects: &[&dyn ClassInstance]) -> anyhow::Result>; +} diff --git a/jvm/src/garbage_collector.rs b/jvm/src/garbage_collector.rs index fb1bfa91..90082e11 100644 --- a/jvm/src/garbage_collector.rs +++ b/jvm/src/garbage_collector.rs @@ -1,7 +1,7 @@ use alloc::{boxed::Box, collections::BTreeMap, string::String, vec::Vec}; use jvm_types::FieldAccessFlags; -use hashbrown::{HashSet, hash_set::Entry}; +use hashbrown::HashSet; use crate::{ClassDefinition, ClassInstance, Field, JavaValue, Jvm, class_loader::Class, thread::JvmThread}; @@ -12,37 +12,46 @@ pub fn determine_garbage( all_class_instances: &HashSet>, classes: &BTreeMap, interned_strings: &[Box], -) -> Vec> { + root_identities: &HashSet, +) -> anyhow::Result>> { let mut reachable_objects = HashSet::new(); - classes.values().for_each(|x| { - find_reachable_objects(jvm, &x.java_class(), &mut reachable_objects); - find_static_reachable_objects(jvm, x, &mut reachable_objects); - }); + for class in classes.values() { + find_reachable_objects(jvm, &class.java_class(), &mut reachable_objects)?; + find_static_reachable_objects(jvm, class, &mut reachable_objects)?; + } - threads + for object in threads .values() .flat_map(|thread| thread.iter_frame().flat_map(|stack| stack.local_variables())) - .for_each(|x| { - find_reachable_objects(jvm, x, &mut reachable_objects); - }); + { + find_reachable_objects(jvm, object, &mut reachable_objects)?; + } - threads.values().filter_map(|thread| thread.java_thread()).for_each(|x| { - find_reachable_objects(jvm, x, &mut reachable_objects); - }); + for object in threads.values().filter_map(|thread| thread.java_thread()) { + find_reachable_objects(jvm, object, &mut reachable_objects)?; + } - global_references.values().for_each(|object| { - find_reachable_objects(jvm, object, &mut reachable_objects); - }); + for object in global_references.values() { + find_reachable_objects(jvm, object, &mut reachable_objects)?; + } - interned_strings.iter().for_each(|x| { - find_reachable_objects(jvm, x, &mut reachable_objects); - }); + for object in interned_strings { + find_reachable_objects(jvm, object, &mut reachable_objects)?; + } + + for object in all_class_instances.iter().filter(|object| root_identities.contains(&object.identity())) { + find_reachable_objects(jvm, object, &mut reachable_objects)?; + } - all_class_instances.difference(&reachable_objects).cloned().collect() + Ok(all_class_instances + .iter() + .filter(|object| !reachable_objects.contains(&object.identity())) + .cloned() + .collect()) } -fn find_static_reachable_objects(jvm: &Jvm, class: &Class, reachable_objects: &mut HashSet>) { +fn find_static_reachable_objects(jvm: &Jvm, class: &Class, reachable_objects: &mut HashSet) -> anyhow::Result<()> { let fields = find_all_fields(jvm, &*class.definition); for field in fields { if !field.access_flags().contains(FieldAccessFlags::STATIC) { @@ -52,21 +61,20 @@ fn find_static_reachable_objects(jvm: &Jvm, class: &Class, reachable_objects: &m let descriptor = field.descriptor(); if (descriptor.starts_with('L') && descriptor.ends_with(';')) || descriptor.starts_with('[') { - let value = class.definition.get_static_field(&*field).unwrap(); + let value = class.definition.get_static_field(&*field)?; if let JavaValue::Object(Some(value)) = value { - find_reachable_objects(jvm, &value, reachable_objects); + find_reachable_objects(jvm, &value, reachable_objects)?; } } } + Ok(()) } #[allow(clippy::borrowed_box)] -fn find_reachable_objects(jvm: &Jvm, object: &Box, reachable_objects: &mut HashSet>) { - let entry = reachable_objects.entry(object.clone()); - if let Entry::Occupied(_) = entry { - return; +fn find_reachable_objects(jvm: &Jvm, object: &Box, reachable_objects: &mut HashSet) -> anyhow::Result<()> { + if !reachable_objects.insert(object.identity()) { + return Ok(()); } - entry.insert(); let class_definition = object.class_definition(); let name = class_definition.name(); @@ -74,11 +82,11 @@ fn find_reachable_objects(jvm: &Jvm, object: &Box, reachable_ if name.starts_with("[L") || name.starts_with("[[") { // is object array let array = object.as_array_instance().unwrap(); - let values = array.load(0, array.length()).unwrap(); + let values = array.load(0, array.length())?; for value in values { if let JavaValue::Object(Some(value)) = value { - find_reachable_objects(jvm, &value, reachable_objects); + find_reachable_objects(jvm, &value, reachable_objects)?; } } } @@ -93,13 +101,14 @@ fn find_reachable_objects(jvm: &Jvm, object: &Box, reachable_ let descriptor = field.descriptor(); if (descriptor.starts_with('L') && descriptor.ends_with(';')) || descriptor.starts_with('[') { - let value = object.get_field(&*field).unwrap(); + let value = object.get_field(&*field)?; if let JavaValue::Object(Some(value)) = value { - find_reachable_objects(jvm, &value, reachable_objects); + find_reachable_objects(jvm, &value, reachable_objects)?; } } } } + Ok(()) } fn find_all_fields(jvm: &Jvm, class_definition: &dyn ClassDefinition) -> Vec> { diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 2403542e..3ed3fc6c 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -19,7 +19,7 @@ use parking_lot::RwLock; use jvm_types::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; use crate::{ - Result, + ExternalRoots, Result, array_class_instance::{ArrayRawBuffer, ArrayRawBufferMut}, class_definition::ClassDefinition, class_instance::{AsClassInstance, ClassInstance, ClassInstanceRef}, @@ -48,6 +48,7 @@ struct JvmInner { monitors: RwLock>>, get_current_thread_id: Box u64 + Sync + Send>, bootstrap_class_loader: Box, + external_roots: Option>, bootstrapping: AtomicBool, } @@ -56,8 +57,27 @@ pub struct Jvm { inner: Arc, } +/// Roots host references until their result or exception has been published to the guest. +/// Scopes must be dropped in stack order, before detaching their originating thread. +#[must_use] +pub struct NativeFrame { + jvm: Jvm, + thread_id: u64, +} + +impl Drop for NativeFrame { + fn drop(&mut self) { + self.jvm.inner.threads.write().get_mut(&self.thread_id).unwrap().pop_frame(); + } +} + impl Jvm { - pub async fn new(bootstrap_class_loader: C, get_current_thread_id: F, properties: BTreeMap<&str, &str>) -> Result + pub async fn new( + bootstrap_class_loader: C, + get_current_thread_id: F, + properties: BTreeMap<&str, &str>, + external_roots: Option>, + ) -> Result where C: BootstrapClassLoader + 'static, F: Fn() -> u64 + 'static + Sync + Send, @@ -75,6 +95,7 @@ impl Jvm { monitors: RwLock::new(BTreeMap::new()), get_current_thread_id: Box::new(get_current_thread_id), bootstrap_class_loader: Box::new(bootstrap_class_loader), + external_roots, bootstrapping: AtomicBool::new(true), }), }; @@ -207,7 +228,7 @@ impl Jvm { let class = self.resolve_class(class_name).await?; - if let Some((declaring_class, field)) = self.resolve_field(&class, name, descriptor) { + if let Some((declaring_class, field)) = self.resolve_field(&class, name, descriptor, true) { if !field.access_flags().contains(FieldAccessFlags::STATIC) { return Err(self .exception("java/lang/IncompatibleClassChangeError", &format!("{class_name}.{name}:{descriptor}")) @@ -244,7 +265,7 @@ impl Jvm { let class = self.resolve_class(class_name).await?; - if let Some((mut declaring_class, field)) = self.resolve_field(&class, name, descriptor) { + if let Some((mut declaring_class, field)) = self.resolve_field(&class, name, descriptor, true) { if !field.access_flags().contains(FieldAccessFlags::STATIC) { return Err(self .exception("java/lang/IncompatibleClassChangeError", &format!("{class_name}.{name}:{descriptor}")) @@ -268,7 +289,7 @@ impl Jvm { tracing::trace!("Get field {class_name}.{name}:{descriptor}"); let class = self.resolve_class(class_name).await?; - if let Some((_, field)) = self.resolve_field(&class, name, descriptor) { + if let Some((_, field)) = self.resolve_field(&class, name, descriptor, false) { if field.access_flags().contains(FieldAccessFlags::STATIC) { return Err(self .exception("java/lang/IncompatibleClassChangeError", &format!("{class_name}.{name}:{descriptor}")) @@ -301,7 +322,7 @@ impl Jvm { tracing::trace!("Put field {class_name}.{name}:{descriptor} = {value:?}"); let class = self.resolve_class(class_name).await?; - if let Some((_, field)) = self.resolve_field(&class, name, descriptor) { + if let Some((_, field)) = self.resolve_field(&class, name, descriptor, false) { if field.access_flags().contains(FieldAccessFlags::STATIC) { return Err(self .exception("java/lang/IncompatibleClassChangeError", &format!("{class_name}.{name}:{descriptor}")) @@ -326,7 +347,7 @@ impl Jvm { let class = self.resolve_class(class_name).await?; - if let Some((declaring_class, method)) = self.resolve_method(&class, name, descriptor) { + if let Some((declaring_class, method)) = self.resolve_method(&class, name, descriptor, true) { if !method.access_flags().contains(MethodAccessFlags::STATIC) { return Err(self .exception("java/lang/IncompatibleClassChangeError", &format!("{class_name}.{name}:{descriptor}")) @@ -354,7 +375,7 @@ impl Jvm { tracing::trace!("Invoke virtual {class_name}.{name}:{descriptor}({args:?})"); let owner = self.resolve_class(class_name).await?; - let Some((resolved_class, resolved_method)) = self.resolve_method(&owner, name, descriptor) else { + let Some((resolved_class, resolved_method)) = self.resolve_method(&owner, name, descriptor, false) else { tracing::error!("No such method: {class_name}.{name}:{descriptor}"); return Err(self .exception("java/lang/NoSuchMethodError", &format!("{class_name}.{name}:{descriptor}")) @@ -618,6 +639,7 @@ impl Jvm { } pub async fn monitor_enter(&self, obj: &(impl AsClassInstance + ?Sized)) -> Result<()> { + let _frame = self.native_frame(&[JavaValue::Object(Some(clone_box(obj.as_class_instance())))]); let thread_id = (self.inner.get_current_thread_id)(); self.get_or_create_monitor(obj.as_class_instance()).enter(thread_id).await; Ok(()) @@ -968,7 +990,7 @@ impl Jvm { .collect() } - pub fn collect_garbage(&self) -> Result { + pub fn collect_garbage(&self) -> anyhow::Result { tracing::trace!("Collecting garbage"); let garbage = { @@ -978,7 +1000,29 @@ impl Jvm { let classes = self.inner.classes.read(); let interned_strings = self.interned_strings(); - determine_garbage(self, &threads, &global_references, &all_objects, &classes, &interned_strings) + let mut root_identities = if let Some(provider) = &self.inner.external_roots { + let objects = all_objects.iter().map(|object| &**object).collect::>(); + provider.roots(&objects)?.into_iter().collect::>() + } else { + HashSet::new() + }; + root_identities.extend( + self.inner + .monitors + .read() + .iter() + .filter_map(|(identity, monitor)| monitor.is_active().then_some(*identity)), + ); + + determine_garbage( + self, + &threads, + &global_references, + &all_objects, + &classes, + &interned_strings, + &root_identities, + )? }; let garbage_count = garbage.len(); @@ -1135,7 +1179,24 @@ impl Jvm { self.inner.threads.read().len() } - // TODO we need safe, ergonomic api.. + pub fn native_frame(&self, args: &[JavaValue]) -> NativeFrame { + let thread_id = (self.inner.get_current_thread_id)(); + let mut threads = self.inner.threads.write(); + let thread = threads.get_mut(&thread_id).unwrap(); + thread.push_native_frame(); + thread + .top_frame_mut() + .local_variables_mut() + .extend(args.iter().filter_map(|arg| match arg { + JavaValue::Object(Some(instance)) => Some(instance.clone()), + _ => None, + })); + NativeFrame { + jvm: self.clone(), + thread_id, + } + } + pub fn push_native_frame(&self) { let thread_id = (self.inner.get_current_thread_id)(); self.inner.threads.write().get_mut(&thread_id).unwrap().push_native_frame(); @@ -1146,6 +1207,22 @@ impl Jvm { self.inner.threads.write().get_mut(&thread_id).unwrap().pop_frame(); } + /// Releases frame-local references while preserving the result in its caller. + fn pop_frame_with_result(&self, result: &Result) { + let returned_reference = match result { + Ok(JavaValue::Object(Some(instance))) => Some(instance.clone()), + Err(JavaError::JavaException(exception)) => Some(exception.clone()), + _ => None, + }; + let thread_id = (self.inner.get_current_thread_id)(); + let mut threads = self.inner.threads.write(); + let thread = threads.get_mut(&thread_id).unwrap(); + thread.pop_frame(); + if let Some(returned_reference) = returned_reference { + thread.top_frame_mut().local_variables_mut().push(returned_reference); + } + } + async fn current_class_loader(&self) -> Result> { let calling_class = self.find_calling_class()?; @@ -1186,36 +1263,36 @@ impl Jvm { // JVMS 5.4.3.2 field resolution: search the class, then its superinterfaces, then its superclass. // Matching is by name and descriptor only; the caller checks static-ness afterwards. - fn resolve_field(&self, class: &Class, name: &str, descriptor: &str) -> Option<(Class, Box)> { + fn resolve_field(&self, class: &Class, name: &str, descriptor: &str, expected_static: bool) -> Option<(Class, Box)> { if let Some(field) = class .definition - .field(name, descriptor, true) - .or_else(|| class.definition.field(name, descriptor, false)) + .field(name, descriptor, expected_static) + .or_else(|| class.definition.field(name, descriptor, !expected_static)) { return Some((class.clone(), field)); } for interface in class.definition.interface_names() { if let Some(interface_class) = self.get_class(&interface) - && let Some(found) = self.resolve_field(&interface_class, name, descriptor) + && let Some(found) = self.resolve_field(&interface_class, name, descriptor, expected_static) { return Some(found); } } let super_class = self.get_class(&class.definition.super_class_name()?)?; - self.resolve_field(&super_class, name, descriptor) + self.resolve_field(&super_class, name, descriptor, expected_static) } // JVMS 5.4.3.3 method resolution searches the class hierarchy before inherited interface methods. - fn resolve_method(&self, class: &Class, name: &str, descriptor: &str) -> Option<(Class, Box)> { + fn resolve_method(&self, class: &Class, name: &str, descriptor: &str, expected_static: bool) -> Option<(Class, Box)> { let mut class = Some(class.clone()); let mut interfaces = Vec::new(); while let Some(candidate) = class { if let Some(method) = candidate .definition - .method(name, descriptor, true) - .or_else(|| candidate.definition.method(name, descriptor, false)) + .method(name, descriptor, expected_static) + .or_else(|| candidate.definition.method(name, descriptor, !expected_static)) { return Some((candidate, method)); } @@ -1321,10 +1398,6 @@ impl Jvm { } else { None }; - if let Some(object) = &synchronized_object { - self.monitor_enter(object).await?; - } - self.inner .threads .write() @@ -1332,33 +1405,30 @@ impl Jvm { .unwrap() .push_java_frame(class, class_instance, &method_str, &args); - let result = method.run(self, args).await; - - tracing::trace!("Execute result: {result:?}"); - - let returned_reference = match &result { - Ok(JavaValue::Object(Some(instance))) => Some(instance.clone()), - Err(JavaError::JavaException(exception)) => Some(exception.clone()), - _ => None, - }; + if let Some(object) = &synchronized_object + && let Err(error) = self.monitor_enter(object).await { - let mut threads = self.inner.threads.write(); - let thread = threads.get_mut(&thread_id).unwrap(); - thread.pop_frame(); - if let Some(returned_reference) = returned_reference { - thread.top_frame_mut().local_variables_mut().push(returned_reference); - } + let result = Err(error); + self.pop_frame_with_result(&result); + return result; } + let mut result = method.run(self, args).await; + + tracing::trace!("Execute result: {result:?}"); + if let Some(object) = &synchronized_object && let Err(error) = self.monitor_exit(object).await { if result.is_ok() { - return Err(error); + result = Err(error); + } else { + tracing::error!(?error, "failed to release synchronized method monitor"); } - tracing::error!(?error, "failed to release synchronized method monitor"); } + self.pop_frame_with_result(&result); + result } } diff --git a/jvm/src/lib.rs b/jvm/src/lib.rs index c0bb28f7..500b8363 100644 --- a/jvm/src/lib.rs +++ b/jvm/src/lib.rs @@ -8,6 +8,7 @@ mod class_definition; mod class_instance; mod class_loader; mod error; +mod external_roots; mod field; mod garbage_collector; mod global_ref; @@ -38,9 +39,10 @@ pub use self::{ class_instance::{Array, AsClassInstance, ClassInstance, ClassInstanceRef}, class_loader::BootstrapClassLoader, error::JavaError, + external_roots::ExternalRoots, field::Field, global_ref::GlobalRef, - jvm::Jvm, + jvm::{Jvm, NativeFrame}, method::Method, monitor::{MonitorWait, MonitorWaitTimeout}, r#type::JavaType, diff --git a/jvm/src/monitor.rs b/jvm/src/monitor.rs index 4d859c59..d678c3f0 100644 --- a/jvm/src/monitor.rs +++ b/jvm/src/monitor.rs @@ -13,6 +13,7 @@ struct MonitorState { depth: usize, next_waiter_id: u64, waiters: VecDeque, + active_waits: usize, } struct MonitorWaiter { @@ -47,11 +48,17 @@ impl Monitor { depth: 0, next_waiter_id: 0, waiters: VecDeque::new(), + active_waits: 0, }), entry_event: Event::new(), } } + pub(crate) fn is_active(&self) -> bool { + let state = self.state.lock(); + state.owner.is_some() || state.active_waits != 0 + } + pub(crate) async fn enter(self: &Arc, thread_id: u64) { loop { let listener = self.entry_event.listen(); @@ -113,6 +120,7 @@ impl Monitor { id: waiter_id, event: event.clone(), }); + state.active_waits += 1; state.owner = None; state.depth = 0; @@ -158,13 +166,19 @@ impl Monitor { } impl MonitorWait { - pub(crate) async fn wait(self) { - self.listener.await; + pub(crate) async fn wait(mut self) { + (&mut self.listener).await; self.monitor.enter(self.thread_id).await; self.monitor.state.lock().depth = self.depth; } } +impl Drop for MonitorWait { + fn drop(&mut self) { + self.monitor.state.lock().active_waits -= 1; + } +} + impl MonitorWaitTimeout { pub fn notify(self) { let event = { diff --git a/jvm/tests/test_garbage_collection.rs b/jvm/tests/test_garbage_collection.rs index 6b6651d9..d82cb8db 100644 --- a/jvm/tests/test_garbage_collection.rs +++ b/jvm/tests/test_garbage_collection.rs @@ -1,8 +1,182 @@ -use jvm::{Array, ClassInstanceRef, JavaValue, Result as JvmResult, runtime::JavaLangString}; +use jvm::{Array, ClassInstance, ClassInstanceRef, ExternalRoots, JavaError, JavaValue, Jvm, runtime::JavaLangString}; -use std::collections::BTreeMap; +use std::{ + collections::BTreeMap, + future::{Future, poll_fn}, + sync::{ + Arc, Mutex, + atomic::{AtomicU64, Ordering}, + }, + task::Poll, +}; -use test_utils::{TestRuntime, create_test_jvm, test_jvm}; +use test_utils::{TestRuntime, create_test_jvm, get_bootstrap_class_loader, test_jvm}; + +use anyhow::Result as JvmResult; + +#[tokio::test] +async fn external_roots_trace_children_and_failed_discovery_does_not_sweep() -> JvmResult<()> { + struct Roots(Arc>>>); + + impl ExternalRoots for Roots { + fn roots(&self, objects: &[&dyn ClassInstance]) -> anyhow::Result> { + let state = self.0.lock().unwrap(); + let roots = state.as_ref().ok_or_else(|| anyhow::anyhow!("root memory unreadable"))?; + for identity in roots { + assert!(objects.iter().any(|object| object.identity() == *identity)); + } + Ok(roots.clone()) + } + } + + let roots = Arc::new(Mutex::new(Some(Vec::new()))); + let jvm = Jvm::new( + get_bootstrap_class_loader(Box::new(TestRuntime::new(BTreeMap::new()))), + || 0, + BTreeMap::new(), + Some(Box::new(Roots(roots.clone()))), + ) + .await?; + jvm.collect_garbage()?; + + let string = { + let _frame = jvm.native_frame(&[]); + let string = JavaLangString::from_rust_string(&jvm, "external").await?; + jvm.new_class("java/lang/Object", "()V", ()).await?; + *roots.lock().unwrap() = Some(vec![string.identity()]); + string + }; + + *roots.lock().unwrap() = None; + assert_eq!(jvm.collect_garbage().unwrap_err().to_string(), "root memory unreadable"); + *roots.lock().unwrap() = Some(vec![string.identity()]); + assert_eq!(jvm.collect_garbage()?, 1); + { + let _frame = jvm.native_frame(&[]); + assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "external"); + } + *roots.lock().unwrap() = Some(Vec::new()); + assert_eq!(jvm.collect_garbage()?, 2); + + let _frame = jvm.native_frame(&[]); + let runtime: Box = jvm.invoke_static("java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;", ()).await?; + *roots.lock().unwrap() = None; + for result in [ + jvm.invoke_static::<_, ()>("java/lang/System", "gc", "()V", ()).await, + jvm.invoke_virtual::<_, ()>(&runtime, "java/lang/Runtime", "gc", "()V", ()).await, + ] { + let JavaError::JavaException(exception) = result.unwrap_err(); + assert!(jvm.is_instance(&*exception, "java/lang/InternalError")); + } + Ok(()) +} + +#[tokio::test] +async fn native_frames_seed_arguments_discard_temporaries_and_remember_their_thread() -> JvmResult<()> { + let current_thread = Arc::new(AtomicU64::new(0)); + let jvm = Jvm::new( + get_bootstrap_class_loader(Box::new(TestRuntime::new(BTreeMap::new()))), + { + let current_thread = current_thread.clone(); + move || current_thread.load(Ordering::Relaxed) + }, + BTreeMap::new(), + None, + ) + .await?; + current_thread.store(1, Ordering::Relaxed); + jvm.attach_thread(None).await?; + current_thread.store(0, Ordering::Relaxed); + jvm.collect_garbage()?; + + let argument = { + let _frame = jvm.native_frame(&[]); + JavaLangString::from_rust_string(&jvm, "argument").await? + }; + let frame = jvm.native_frame(&[JavaValue::from(argument), JavaValue::Int(7), JavaValue::Object(None)]); + jvm.new_class("java/lang/Object", "()V", ()).await?; + tokio::task::yield_now().await; + assert_eq!(jvm.collect_garbage()?, 0); + + current_thread.store(1, Ordering::Relaxed); + let other_frame = jvm.native_frame(&[]); + jvm.new_class("java/lang/Object", "()V", ()).await?; + drop(frame); + assert_eq!(jvm.collect_garbage()?, 3); + drop(other_frame); + assert_eq!(jvm.collect_garbage()?, 1); + Ok(()) +} + +#[tokio::test] +async fn active_monitors_are_roots_until_release_including_wait_reacquisition() -> JvmResult<()> { + let jvm = test_jvm().await?; + jvm.collect_garbage()?; + let object = { + let _frame = jvm.native_frame(&[]); + JavaLangString::from_rust_string(&jvm, "monitor").await? + }; + jvm.monitor_enter(&object).await?; + assert_eq!(jvm.collect_garbage()?, 0); + let (wait, timeout) = jvm.object_wait_prepare(&object).await?; + assert_eq!(jvm.collect_garbage()?, 0); + timeout.clone().notify(); + assert_eq!(jvm.collect_garbage()?, 0); + jvm.object_wait(wait).await?; + assert_eq!(jvm.collect_garbage()?, 0); + jvm.monitor_exit(&object).await?; + assert_eq!(jvm.collect_garbage()?, 2); + timeout.notify(); + Ok(()) +} + +#[tokio::test] +async fn synchronized_entry_roots_arguments_while_waiting_for_the_monitor() -> JvmResult<()> { + let current_thread = Arc::new(AtomicU64::new(0)); + let jvm = Jvm::new( + get_bootstrap_class_loader(Box::new(TestRuntime::new(BTreeMap::new()))), + { + let current_thread = current_thread.clone(); + move || current_thread.load(Ordering::Relaxed) + }, + BTreeMap::new(), + None, + ) + .await?; + current_thread.store(1, Ordering::Relaxed); + jvm.attach_thread(None).await?; + current_thread.store(0, Ordering::Relaxed); + let vector = { + let _frame = jvm.native_frame(&[]); + let vector = jvm.new_class("java/util/Vector", "(I)V", (1,)).await?; + jvm.monitor_enter(&vector).await?; + vector + }; + jvm.collect_garbage()?; + let argument = { + let _frame = jvm.native_frame(&[]); + JavaLangString::from_rust_string(&jvm, "pending argument").await? + }; + current_thread.store(1, Ordering::Relaxed); + let mut call = Box::pin(jvm.invoke_virtual::<_, ()>(&vector, "java/util/Vector", "addElement", "(Ljava/lang/Object;)V", (argument,))); + assert!(poll_fn(|context| Poll::Ready(call.as_mut().poll(context).is_pending())).await); + + current_thread.store(0, Ordering::Relaxed); + assert_eq!(jvm.collect_garbage()?, 0); + jvm.monitor_exit(&vector).await?; + assert_eq!(jvm.collect_garbage()?, 0); + current_thread.store(1, Ordering::Relaxed); + call.await?; + { + let _frame = jvm.native_frame(&[]); + let element: Box = jvm + .invoke_virtual(&vector, "java/util/Vector", "elementAt", "(I)Ljava/lang/Object;", (0,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &element).await?, "pending argument"); + } + assert_eq!(jvm.collect_garbage()?, 4); + Ok(()) +} #[tokio::test] async fn test_garbage_collection() -> JvmResult<()> { @@ -150,12 +324,17 @@ async fn substring_keeps_shared_array_alive_after_parent_collection() -> JvmResu async fn global_references_are_independent_garbage_collection_roots() -> JvmResult<()> { let jvm = test_jvm().await?; + jvm.push_native_frame(); + jvm.resolve_class("[Ljava/lang/Object;").await?; + jvm.pop_frame(); jvm.collect_garbage()?; struct Object; jvm.push_native_frame(); - let object: ClassInstanceRef = jvm.new_class("java/lang/Object", "()V", ()).await?.into(); + let mut object: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 2).await?.into(); + let references = [object.clone(), object.clone()]; + jvm.store_array(&mut object, 0, references).await?; let first = jvm.new_global_ref(&object).unwrap(); let second = jvm.new_global_ref(&object).unwrap(); jvm.pop_frame(); diff --git a/jvm/tests/test_string.rs b/jvm/tests/test_string.rs index 9daccb08..6fec7162 100644 --- a/jvm/tests/test_string.rs +++ b/jvm/tests/test_string.rs @@ -72,7 +72,7 @@ async fn test_intern_on_substring_uses_logical_slice() -> Result<()> { } #[tokio::test] -async fn test_intern_identity_survives_gc() -> Result<()> { +async fn test_intern_identity_survives_gc() -> anyhow::Result<()> { let jvm = test_jvm().await?; // collect first for a clean baseline, then intern and assert the very next GC keeps it diff --git a/rustjava-runtime/src/classes/java/lang/runtime.rs b/rustjava-runtime/src/classes/java/lang/runtime.rs index aee3bc66..64ee6547 100644 --- a/rustjava-runtime/src/classes/java/lang/runtime.rs +++ b/rustjava-runtime/src/classes/java/lang/runtime.rs @@ -1,4 +1,4 @@ -use alloc::vec; +use alloc::{format, vec}; use jvm::{ClassInstanceRef, Jvm, Result}; use jvm_class_proto::JavaMethodProto; @@ -67,7 +67,11 @@ impl Runtime { async fn gc(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Runtime::gc({this:?})"); - jvm.collect_garbage()?; + if let Err(error) = jvm.collect_garbage() { + return Err(jvm + .exception("java/lang/InternalError", &format!("Garbage collection failed: {error:#}")) + .await); + } Ok(()) } diff --git a/rustjava-runtime/src/classes/java/lang/system.rs b/rustjava-runtime/src/classes/java/lang/system.rs index 2b92ff79..f46b354f 100644 --- a/rustjava-runtime/src/classes/java/lang/system.rs +++ b/rustjava-runtime/src/classes/java/lang/system.rs @@ -1,4 +1,4 @@ -use alloc::{string::String as RustString, vec, vec::Vec}; +use alloc::{format, string::String as RustString, vec, vec::Vec}; use jvm::{JavaValue, runtime::JavaLangString}; @@ -157,7 +157,11 @@ impl System { async fn gc(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { tracing::debug!("java.lang.System::gc()"); - jvm.collect_garbage()?; + if let Err(error) = jvm.collect_garbage() { + return Err(jvm + .exception("java/lang/InternalError", &format!("Garbage collection failed: {error:#}")) + .await); + } Ok(()) } diff --git a/rustjava-runtime/tests/classes/java/lang/test_class.rs b/rustjava-runtime/tests/classes/java/lang/test_class.rs index 782f8191..8cc16572 100644 --- a/rustjava-runtime/tests/classes/java/lang/test_class.rs +++ b/rustjava-runtime/tests/classes/java/lang/test_class.rs @@ -565,7 +565,7 @@ async fn test_system_class_loader_uses_rustjar_parent() -> Result<()> { let bootstrap_class_loader = get_bootstrap_class_loader(Box::new(runtime.clone())); let class_path = std::env::join_paths(["external.rustjar", "classes"]).unwrap().into_string().unwrap(); let properties = [("java.class.path", class_path.as_str())].into_iter().collect(); - let jvm = Jvm::new(bootstrap_class_loader, move || runtime.current_task_id(), properties).await?; + let jvm = Jvm::new(bootstrap_class_loader, move || runtime.current_task_id(), properties, None).await?; let system_class_loader: ClassInstanceRef = jvm .invoke_static("java/lang/ClassLoader", "getSystemClassLoader", "()Ljava/lang/ClassLoader;", ()) diff --git a/src/lib.rs b/src/lib.rs index bc359f5d..9a4f408e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,7 @@ where let class_path_str = build_class_path(start_type, class_path)?; let properties = [("java.class.path", class_path_str.as_str())].into_iter().collect(); - Ok(Jvm::new(bootstrap_class_loader, move || runtime.current_task_id(), properties).await?) + Ok(Jvm::new(bootstrap_class_loader, move || runtime.current_task_id(), properties, None).await?) } fn build_class_path(start_type: &StartType<'_>, class_path: &[&Path]) -> anyhow::Result { diff --git a/test-data/MonitorSemantics$CounterWorker.class b/test-data/MonitorSemantics$CounterWorker.class index e9702cd0845a90891006a431aacb8269e6095e61..5f0deb13c3c42c448ccf9f659946ed1111b32f8c 100644 GIT binary patch delta 486 zcmZuuJx>Br5S({+aCn}0A!$4m%EH)52q7_QgE6s@#y_AG_Qr&o z%8P|2(oy>l{0|nyi1UD0nM-zN_r2Mjeb?2$bff$8@(N%B#XIA3e-0rX^CrUZ3RtJ* ztFnDozNp!U*Y(Tlxqwou7#K&?L=15Oqg<&}>-9uF=Ll%2Qo5vLv1y72VK<+|z1VIR zlt^QN{Z%ONzzcx{Gn*)gp3F1!Hi?2IMtRI4l8okvFC)cFApIxq3|aw>x*M{bM`T?X z#|Q5&tUVV~E`nk0#6{#N+iAhr(Xx$BP2;32`r(u+D6ox~Wq4j(^1 zc>o5cx#=v8L@|LCp5-w)uoj~xz*>e?xtEPBnY8X;Xm^!74DB0==;J01RVS0p8P5QGKIFcXurT5mhTrZa5`rI delta 426 zcmZWkyG{a86r6K+;gW@z4N(*1ji@ZHyrj_>dJ1b}Vxb{^0Uto)59k*5Hq=yZjL}3Y z`2jz`f3P4%oV^emn=^CnJ#*&Fm!4^7_Ve@tpo{LU^I1${D}^j_0_SQlxE>Cx?N&!% z*ZN*xqY$*^Uh2WS+{s~Zp$elsr4|%oh(l0h*CYxtR)2(^kSLT`qi;T<%$i#93bxq^ zs(<2Vvx-ZsldRi$fEU6!KYa_~_Cl=Z?aL6EGk-dUb8P$5j9q=b5g9K&Ce*E=V78aB zW6(0b6(oX^wK2-kwFZp_=b{@*JUb1Cv%4mMk3BNsPZP_#YUFv?-%zsPYD<%7B*5a$ z5uYRGMuGMzigF(j6*0;!|8nE?1`We}--92i}LN=54yF-@kBM4RVz*uzcy0*)gw Aa{vGU diff --git a/test-data/MonitorSemantics$FailingWorker.class b/test-data/MonitorSemantics$FailingWorker.class index c39937f7fbc2218094a90ff0f4c399c9191a17d6..35a0435a8548d9bb407cc96fee61f0726346001a 100644 GIT binary patch delta 36 scmcb^dWUtxe?~^3Nlfm%q72Lo;tXsIVhrpI5|cxiJQ;;1&ttL!0HWvzjsO4v delta 36 scmcb^dWUtxe?~^ZNlfm%A`Hw7Vhn5yq73W|;*&#|JQ)Qi&ttL!0HT8jhyVZp diff --git a/test-data/MonitorSemantics.class b/test-data/MonitorSemantics.class index ea2b76b76de5a7b1f62f68b976f0aef9e6067352..8ab4718b6ded9ee0d38ffd4dfd3e2c1157de2a7d 100644 GIT binary patch literal 1742 zcmZ`(ZBrUo6n-uui|g7@jY&gIss;^^AV||!ZDK>UrJ|;RU}^f2b-B^Cu0%U1smOd(U&;o^vj5{{8bW0Qd1+M+gHN!UhHr5s0n1jvcsO zSvE~4uq|I;{Do;ZHh1iXEWdQD9gl!tWnEw(nW_%aI%;4Dx`1Z6`%WM|0Z~-vhJg{h zB@ixc6jlXBOS|STX0~BE+u4n+U1EC=R_uO&KxENoIuCS=m=K7{gFS|pbsaZwO9g(96()OfD+iwK zY%ip$8m7oc*I#AFlV)ARZGl_;EF}u<+BaSAXX)vf#*FekD{wtox&WT1cO8>RDom<{ z>4st2j=9y49n4(=_f*Y;eqefmvYIoHRVU$H*LF0_UzW}aTgEC*)5&2*_4&Sm50quZ ztk)~j86vA68Tc5Vu$k_DKqA^a*lzYQNeD=VG@ESG0v0uVX5axH3XHXKJ&$4c_moA$ zVP45%s!CCC>r&uK$#&%WesfED6;+lR1lKYfRnxQ8cjqu1?AV;O(FGxXl z*=22_-t@L)I-F%q%{U1Y(9h<@*RXL~ZEI@JG(?$} zC4E1U%jE>FCR06$b!NMZnO7KImSXQwzReTQLI+fz`pYY9bkSli9DAOZ-F?rJE4+N{ zPX8dysZIzOMaPj|p<(*IBo8C!W>LeBEWY0|!KoQJHzb(ifW-Kp5JFttTkdpquPKK> z8W7>S#IG!+pd3se;mRM}gz!06r3T+eT#YsW1$@D;wEAb^$rAS|3~&WTj&bz_H)^Br z9^vNSJP<7>YDj?zf^O1`v)VD6ZJjftLufJm`73BDaIei2vz+F;j1^*+sQrgw!<0`? zFj+gnbWI!~afCaizVEfWuaUmTmoh_{Y3p+epC^7jLml7DG2TDKhs@wLKJC_>XSKi# zshj29yn_Ui{H1tl)5vjMLWV5N;VTliig{F!Z*i1vCx1deA6Lif>J%Z3wwVaatWIu3YaA{mMy zA&CdQ1!0AvNJ^0M-UurdMcTw6Xad3InzK|h5XE5=8M&h_K6_l%k@J3tkEY=3Xt=+^1fblGt?uCv}XPIEB;RsIn=hJv;D4-19aA zlPhB^u>ipj2xjW&de4P|GqhT3Q{XJurcb~)&Jkr0Y(?eaegtR%qlxbFp58k4eI^py zZitf4oVh3YY2Ge}u?9VCc%M{bMT*JVfH*pk;5?5D#FGAvQY!s?6VY-Lv9hQmUPu4* zKX@j)j)5E}y-jcX@vP+M$)C+L#%VWjWDQ3tzK-L5tu6gQmIx#fMv55>(57MYVRE!Z z7BG%1E)q>3k12Mn#9i5#jGrB2mx#i&mvM!qCiox|Tq!)+gR5KxFiD2W*i<-#8O*jD zVtHSKyIVQ58_wu8T;Gb)%b_ykcl6sEE6BWon{8&1T9T_p${&30|DI999QzHPE^R%F z4NIzUs0|3&mmNX~Vcw^DuQhA<@86+N^cL9)ZsQJpgk{Ev$`rUuEJd=!_hI}2lzVZ2 diff --git a/test-data/ThreadInterruption.class b/test-data/ThreadInterruption.class index f5f00d787fb031bbf64828417a3e6f3a1ea67934..b603b05952ffd0b2651e0156e773452f7b024b2d 100644 GIT binary patch delta 1630 zcmZuxT}%{L6#mZcFtZH9##*{$pb)i7K$6DzldcgPO4P)X+B71<(vmXh0?RJD0=fzc z3$+$n6~&99C|1RaZEY1y!H8*_kT$jUXK|qC!KZ8Oo99-e6suinXZH z@H*ZQOP%@4YcyN$Lq4K}X9`|vG-Af8uo0AYyx&|=c1Ce%y9;S@G&UaF}UGt~@p zc0fZLiU>-uwIm#j1r!`)J&(TG2sA|_715SZphZPHwwlGA8sZ`>`Lo_3JpG7rbiYv> z=Ec3@S1FH<-cCu8i7pCFo~ADGz3f@u&C)W6M>d7Ga2O7B0a7ngkPbK$p(s_(J17%| zl1n>9my(h1&p@@|lg5!Tgk>J-4*UV1?2%Ikh9Qr_J&sI|Jb^dGl#-oSJB3336as#l zMExX6UwwW8+xVBvH<4U-*LvC`qOQ!`T4wgix|D&|3GCy*BL{r0 zajem$3G5eFU3u|6x;j0Iv<(`gu6?0Psa*Mg#RN7c)Z0y!L#)0*)m2{Gp{wk|9BRkl zMloJP8Sg+DDlDu=C(M26#t{qq@D=WJd>=C=l8dq^hcSnWS6GuxR8EzgsHAEO>nK9a z95++U!h7@$&2T(JKbXi{%=9FqMD(Ptj#r}<2KJ&J`_P0Ke-7XfpMEbNZyyd~kfULA z-~+aG199Bs)suWwclofU@ix9=n?IrlzwwGc@Gc(UC}wet2*=5VK62v(Wul+*AjpRS z+KAJ%31{eK4AOR-i&G7Ts1D~TgbUP$Vd}(1I*d!yi_3HpSLiH8Xara3GhCy47^N?9 zoxaA0^b+5xIR&eAW$g|KCc-^Im z=%E-_GmM^vDo`RnhxH3N_ZBOsJVZ{CDw&wa5-185RwtBr^d@PPdHCSs3Xz|d6Mwu; zlK2J3i!=`%sZY{K(jp?>SZ)jMlJ}Ola16(J*2S4;t={1~<5+f!W3t|uWJhsjA5J7I zb0MbFmWCEJ9N#{<=(fY6lC%7`k#)m{s^3mD(@ykMf>pevWyT-X6LuU=*>McmJ9FDP zJB^~`LoV8mG^;ZH*nXU`GoaWR7_>J)PZ)p;XHk$eu#KC`yjD)Z>z~APqC7pHx8`d<5ZG*fGvf*>rAA(43o z-nsX_Khu9o*KaO-@fCm~OkL9dvZUu=tAT3d5!7&`Gq|fq$5W^=upK^;ZC#VS!$2kK z2ufbX6Wes;W2XVXq$@3vU|2(=2wK;w4Jg;JOH5gF)POXD;-+==&Stb4Sc4GZ(YzO1 z{9XRSkUzY;aBFjWz~59V%jF}WQi2xj*cb|S z1vKnsJ-7FHf1oWAu8Fj_2HJIWqg;}E4eS@aiZAU5;gSC-N4otDAx<6;e=E6k?6S+I z5}g(4>Kq*qzo_o)gDkCrc;%)L#UWU508sj1Ln>fVgj=hveKL;17_xkuug8!!4ukDD zwoEVnYZ_GrbJNgEYz1>(+qD=xO#2+$T&VRID>1mnk>yr65GYpNs$*mV>NK28bE|Q@ zD!$XwbL+1n-**)aKAJ_-EGjk^#PJgUm*N2#-z`$@MOw4ZtIVQBl-t)+o9MH9GV8f= zvFcS^crA{0cHC;9*dD_=uM$V6h}(0cZC;%lOerxKy~Z`K;$Y@7BaUYh>fKVMht*4T zugyZ9OUFw3odjU)IL{rCZ|<5wKT?>L4(@Fs3?<^qnB4MUWI z6Xe1vT8&{Uf}p1{LS-1G7jT+h#u?gyu}W&hI5p!eb>STKVuB7~k_IqELwJ`)ah|5| z9>wrJUBL(RC8lW(AJUJwKtJOn`VBMm4?d<__=Fblsl|$m=o6E=J9_vw)H`r!$Z=L^ zkltN5*;DSoquhqR$cIqT(61ri{N;cWv<0lc7jtehW7=J0BuLAFMXZ9hB0Euo*X1$} zZ4t$=Geta`l0p6Am6XirJ+URa!VN4ZXgo%~9ei1iuCn6|9wf=@g2x!;!{lB=fqE+b>+L9QJ`3G%SviLSVwT;)VEJx?;dlgag5N%U|kS=EQk zs?qyi>>, available: Notify, @@ -416,7 +418,7 @@ where let properties = [("java.class.path", ".")].into_iter().collect(); - Jvm::new(bootstrap_class_loader, move || runtime.current_task_id(), properties).await + Jvm::new(bootstrap_class_loader, move || runtime.current_task_id(), properties, None).await } pub async fn test_jvm() -> Result { From b0fac614a7e9167857515d882610a4a4d9581228 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sun, 25 Oct 2026 09:34:17 +0900 Subject: [PATCH 2/3] Consolidate native frame management around scoped guards --- jvm/src/jvm.rs | 19 ++--- jvm/tests/test_garbage_collection.rs | 108 +++++++++++++-------------- 2 files changed, 59 insertions(+), 68 deletions(-) diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 3ed3fc6c..9345ce7d 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -639,7 +639,7 @@ impl Jvm { } pub async fn monitor_enter(&self, obj: &(impl AsClassInstance + ?Sized)) -> Result<()> { - let _frame = self.native_frame(&[JavaValue::Object(Some(clone_box(obj.as_class_instance())))]); + let _frame = self.push_native_frame(&[JavaValue::Object(Some(clone_box(obj.as_class_instance())))]); let thread_id = (self.inner.get_current_thread_id)(); self.get_or_create_monitor(obj.as_class_instance()).enter(thread_id).await; Ok(()) @@ -1132,8 +1132,9 @@ impl Jvm { // started from java (Thread.start), or None to create one pub async fn attach_thread(&self, java_thread: Option>) -> Result<()> { let thread_id = (self.inner.get_current_thread_id)(); - self.inner.threads.write().insert(thread_id, JvmThread::new()); - self.push_native_frame(); + let mut thread = JvmThread::new(); + thread.push_native_frame(); + self.inner.threads.write().insert(thread_id, thread); let java_thread = match java_thread { Some(x) => x, @@ -1179,7 +1180,7 @@ impl Jvm { self.inner.threads.read().len() } - pub fn native_frame(&self, args: &[JavaValue]) -> NativeFrame { + pub fn push_native_frame(&self, args: &[JavaValue]) -> NativeFrame { let thread_id = (self.inner.get_current_thread_id)(); let mut threads = self.inner.threads.write(); let thread = threads.get_mut(&thread_id).unwrap(); @@ -1197,16 +1198,6 @@ impl Jvm { } } - pub fn push_native_frame(&self) { - let thread_id = (self.inner.get_current_thread_id)(); - self.inner.threads.write().get_mut(&thread_id).unwrap().push_native_frame(); - } - - pub fn pop_frame(&self) { - let thread_id = (self.inner.get_current_thread_id)(); - self.inner.threads.write().get_mut(&thread_id).unwrap().pop_frame(); - } - /// Releases frame-local references while preserving the result in its caller. fn pop_frame_with_result(&self, result: &Result) { let returned_reference = match result { diff --git a/jvm/tests/test_garbage_collection.rs b/jvm/tests/test_garbage_collection.rs index d82cb8db..5cdd4af1 100644 --- a/jvm/tests/test_garbage_collection.rs +++ b/jvm/tests/test_garbage_collection.rs @@ -40,7 +40,7 @@ async fn external_roots_trace_children_and_failed_discovery_does_not_sweep() -> jvm.collect_garbage()?; let string = { - let _frame = jvm.native_frame(&[]); + let _frame = jvm.push_native_frame(&[]); let string = JavaLangString::from_rust_string(&jvm, "external").await?; jvm.new_class("java/lang/Object", "()V", ()).await?; *roots.lock().unwrap() = Some(vec![string.identity()]); @@ -52,13 +52,13 @@ async fn external_roots_trace_children_and_failed_discovery_does_not_sweep() -> *roots.lock().unwrap() = Some(vec![string.identity()]); assert_eq!(jvm.collect_garbage()?, 1); { - let _frame = jvm.native_frame(&[]); + let _frame = jvm.push_native_frame(&[]); assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "external"); } *roots.lock().unwrap() = Some(Vec::new()); assert_eq!(jvm.collect_garbage()?, 2); - let _frame = jvm.native_frame(&[]); + let _frame = jvm.push_native_frame(&[]); let runtime: Box = jvm.invoke_static("java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;", ()).await?; *roots.lock().unwrap() = None; for result in [ @@ -90,16 +90,16 @@ async fn native_frames_seed_arguments_discard_temporaries_and_remember_their_thr jvm.collect_garbage()?; let argument = { - let _frame = jvm.native_frame(&[]); + let _frame = jvm.push_native_frame(&[]); JavaLangString::from_rust_string(&jvm, "argument").await? }; - let frame = jvm.native_frame(&[JavaValue::from(argument), JavaValue::Int(7), JavaValue::Object(None)]); + let frame = jvm.push_native_frame(&[JavaValue::from(argument), JavaValue::Int(7), JavaValue::Object(None)]); jvm.new_class("java/lang/Object", "()V", ()).await?; tokio::task::yield_now().await; assert_eq!(jvm.collect_garbage()?, 0); current_thread.store(1, Ordering::Relaxed); - let other_frame = jvm.native_frame(&[]); + let other_frame = jvm.push_native_frame(&[]); jvm.new_class("java/lang/Object", "()V", ()).await?; drop(frame); assert_eq!(jvm.collect_garbage()?, 3); @@ -113,7 +113,7 @@ async fn active_monitors_are_roots_until_release_including_wait_reacquisition() let jvm = test_jvm().await?; jvm.collect_garbage()?; let object = { - let _frame = jvm.native_frame(&[]); + let _frame = jvm.push_native_frame(&[]); JavaLangString::from_rust_string(&jvm, "monitor").await? }; jvm.monitor_enter(&object).await?; @@ -147,14 +147,14 @@ async fn synchronized_entry_roots_arguments_while_waiting_for_the_monitor() -> J jvm.attach_thread(None).await?; current_thread.store(0, Ordering::Relaxed); let vector = { - let _frame = jvm.native_frame(&[]); + let _frame = jvm.push_native_frame(&[]); let vector = jvm.new_class("java/util/Vector", "(I)V", (1,)).await?; jvm.monitor_enter(&vector).await?; vector }; jvm.collect_garbage()?; let argument = { - let _frame = jvm.native_frame(&[]); + let _frame = jvm.push_native_frame(&[]); JavaLangString::from_rust_string(&jvm, "pending argument").await? }; current_thread.store(1, Ordering::Relaxed); @@ -168,7 +168,7 @@ async fn synchronized_entry_roots_arguments_while_waiting_for_the_monitor() -> J current_thread.store(1, Ordering::Relaxed); call.await?; { - let _frame = jvm.native_frame(&[]); + let _frame = jvm.push_native_frame(&[]); let element: Box = jvm .invoke_virtual(&vector, "java/util/Vector", "elementAt", "(I)Ljava/lang/Object;", (0,)) .await?; @@ -185,13 +185,13 @@ async fn test_garbage_collection() -> JvmResult<()> { // collect garbage before test to ensure a clean state jvm.collect_garbage()?; - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let _string = JavaLangString::from_rust_string(&jvm, "test").await?; let garbage_count = jvm.collect_garbage()?; assert_eq!(garbage_count, 0); - jvm.pop_frame(); + drop(frame); let garbage_count = jvm.collect_garbage()?; @@ -199,26 +199,26 @@ async fn test_garbage_collection() -> JvmResult<()> { assert_eq!(garbage_count, 2); // load a class - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let _ = jvm.resolve_class("java/util/Random").await?; - jvm.pop_frame(); + drop(frame); let garbage_count = jvm.collect_garbage()?; assert_eq!(garbage_count, 2); // use loaded class - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let _random = jvm.new_class("java/util/Random", "()V", ()).await?; - jvm.pop_frame(); + drop(frame); let garbage_count = jvm.collect_garbage()?; assert_eq!(garbage_count, 1); // load another class - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let _ = jvm.resolve_class("java/util/Vector").await?; - jvm.pop_frame(); + drop(frame); let garbage_count = jvm.collect_garbage()?; @@ -229,7 +229,7 @@ async fn test_garbage_collection() -> JvmResult<()> { assert_eq!(remaining_garbage_count, 0); // use loaded class - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let vector = jvm.new_class("java/util/Vector", "()V", ()).await?; let _: () = jvm .invoke_virtual( @@ -245,7 +245,7 @@ async fn test_garbage_collection() -> JvmResult<()> { assert!(garbage_count > 0); assert_eq!(jvm.collect_garbage()?, 0); - jvm.pop_frame(); + drop(frame); // vector, elementData, string, and its internal [C should be garbage collected let garbage_count = jvm.collect_garbage()?; @@ -261,7 +261,7 @@ async fn test_garbage_collection_hashtable() -> JvmResult<()> { // collect garbage before test to ensure a clean state jvm.collect_garbage()?; - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let hashtable = jvm.new_class("java/util/Hashtable", "()V", ()).await?; let key = JavaLangString::from_rust_string(&jvm, "key").await?; @@ -281,7 +281,7 @@ async fn test_garbage_collection_hashtable() -> JvmResult<()> { let garbage_count = jvm.collect_garbage()?; assert_eq!(garbage_count, 0); - jvm.pop_frame(); + drop(frame); // hashtable, table array, entry, key string, key [C, value string, value [C let garbage_count = jvm.collect_garbage()?; @@ -298,20 +298,20 @@ async fn substring_keeps_shared_array_alive_after_parent_collection() -> JvmResu struct String; - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let parent = JavaLangString::from_rust_string(&jvm, "HelloWorld").await?; let child: ClassInstanceRef = jvm .invoke_virtual(&parent, &parent.class_definition().name(), "substring", "(II)Ljava/lang/String;", (2, 5)) .await?; let child = jvm.new_global_ref(&child).unwrap(); - jvm.pop_frame(); + drop(frame); // only the parent String is collected; the shared [C stays reachable through the child's value field assert_eq!(jvm.collect_garbage()?, 1); - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); assert_eq!(JavaLangString::to_rust_string(&jvm, &child).await?, "llo"); - jvm.pop_frame(); + drop(frame); jvm.collect_garbage()?; drop(child); @@ -324,20 +324,20 @@ async fn substring_keeps_shared_array_alive_after_parent_collection() -> JvmResu async fn global_references_are_independent_garbage_collection_roots() -> JvmResult<()> { let jvm = test_jvm().await?; - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); jvm.resolve_class("[Ljava/lang/Object;").await?; - jvm.pop_frame(); + drop(frame); jvm.collect_garbage()?; struct Object; - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let mut object: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 2).await?.into(); let references = [object.clone(), object.clone()]; jvm.store_array(&mut object, 0, references).await?; let first = jvm.new_global_ref(&object).unwrap(); let second = jvm.new_global_ref(&object).unwrap(); - jvm.pop_frame(); + drop(frame); assert_eq!(jvm.collect_garbage()?, 0); drop(first); @@ -357,26 +357,26 @@ async fn array_load_result_is_a_local_reference() -> JvmResult<()> { struct Object; - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let _: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 0).await?.into(); - jvm.pop_frame(); + drop(frame); jvm.collect_garbage()?; - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let mut array: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 1).await?.into(); let object: ClassInstanceRef = jvm.new_class("java/lang/Object", "()V", ()).await?.into(); jvm.store_array(&mut array, 0, [object]).await?; let array = jvm.new_global_ref(&array).unwrap(); - jvm.pop_frame(); + drop(frame); assert_eq!(jvm.collect_garbage()?, 0); - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let _: Vec> = jvm.load_array(&array, 0, 1).await?; let mut mutable_array = (*array).clone(); jvm.store_array(&mut mutable_array, 0, [ClassInstanceRef::::new(None)]).await?; assert_eq!(jvm.collect_garbage()?, 0); - jvm.pop_frame(); + drop(frame); assert_eq!(jvm.collect_garbage()?, 1); drop(array); @@ -392,23 +392,23 @@ async fn field_and_method_results_are_local_references() -> JvmResult<()> { struct Object; struct Vector; - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let _: ClassInstanceRef = jvm.new_class("java/util/Vector", "()V", ()).await?.into(); - jvm.pop_frame(); + drop(frame); jvm.collect_garbage()?; - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let vector: ClassInstanceRef = jvm.new_class("java/util/Vector", "(I)V", (1,)).await?.into(); let object: ClassInstanceRef = jvm.new_class("java/lang/Object", "()V", ()).await?.into(); let _: () = jvm .invoke_virtual(&vector, "java/util/Vector", "addElement", "(Ljava/lang/Object;)V", (object,)) .await?; let vector = jvm.new_global_ref(&vector).unwrap(); - jvm.pop_frame(); + drop(frame); assert_eq!(jvm.collect_garbage()?, 0); - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let _: ClassInstanceRef> = jvm.get_field(&vector, "java/util/Vector", "elementData", "[Ljava/lang/Object;").await?; let mut mutable_vector = (*vector).clone(); jvm.put_field( @@ -420,27 +420,27 @@ async fn field_and_method_results_are_local_references() -> JvmResult<()> { ) .await?; assert_eq!(jvm.collect_garbage()?, 0); - jvm.pop_frame(); + drop(frame); assert_eq!(jvm.collect_garbage()?, 2); drop(vector); assert_eq!(jvm.collect_garbage()?, 1); - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let vector: ClassInstanceRef = jvm.new_class("java/util/Vector", "(I)V", (1,)).await?.into(); let object: ClassInstanceRef = jvm.new_class("java/lang/Object", "()V", ()).await?.into(); let _: () = jvm .invoke_virtual(&vector, "java/util/Vector", "addElement", "(Ljava/lang/Object;)V", (object,)) .await?; let vector = jvm.new_global_ref(&vector).unwrap(); - jvm.pop_frame(); + drop(frame); - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let _: ClassInstanceRef = jvm .invoke_virtual(&vector, "java/util/Vector", "remove", "(I)Ljava/lang/Object;", (0,)) .await?; assert_eq!(jvm.collect_garbage()?, 0); - jvm.pop_frame(); + drop(frame); assert_eq!(jvm.collect_garbage()?, 1); drop(vector); @@ -457,12 +457,12 @@ async fn static_field_result_is_a_local_reference() -> JvmResult<()> { jvm.collect_garbage()?; - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let _: ClassInstanceRef = jvm.get_static_field("java/lang/System", "out", "Ljava/io/PrintStream;").await?; jvm.put_static_field("java/lang/System", "out", "Ljava/io/PrintStream;", ClassInstanceRef::::new(None)) .await?; assert_eq!(jvm.collect_garbage()?, 0); - jvm.pop_frame(); + drop(frame); assert!(jvm.collect_garbage()? > 0); @@ -475,16 +475,16 @@ async fn returned_exception_is_a_local_reference() -> JvmResult<()> { struct Vector; - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let vector: ClassInstanceRef = jvm.new_class("java/util/Vector", "()V", ()).await?.into(); let _: jvm::JavaError = jvm .invoke_virtual::<_, ClassInstanceRef<()>>(&vector, "java/util/Vector", "elementAt", "(I)Ljava/lang/Object;", (0,)) .await .unwrap_err(); - jvm.pop_frame(); + drop(frame); jvm.collect_garbage()?; - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let vector: ClassInstanceRef = jvm.new_class("java/util/Vector", "()V", ()).await?.into(); let _: jvm::JavaError = jvm .invoke_virtual::<_, ClassInstanceRef<()>>(&vector, "java/util/Vector", "elementAt", "(I)Ljava/lang/Object;", (0,)) @@ -492,7 +492,7 @@ async fn returned_exception_is_a_local_reference() -> JvmResult<()> { .unwrap_err(); assert_eq!(jvm.collect_garbage()?, 0); - jvm.pop_frame(); + drop(frame); assert_eq!(jvm.collect_garbage()?, 8); Ok(()) @@ -505,10 +505,10 @@ async fn thread_start_keeps_the_thread_alive_until_spawn_callback_runs() -> JvmR jvm.collect_garbage()?; - jvm.push_native_frame(); + let frame = jvm.push_native_frame(&[]); let thread = jvm.new_class("java/lang/Thread", "()V", ()).await?; let _: () = jvm.invoke_virtual(&thread, &thread.class_definition().name(), "start", "()V", ()).await?; - jvm.pop_frame(); + drop(frame); assert_eq!(jvm.collect_garbage()?, 0); From 0a104bf7351f5b2991deef0d9108c16b112d01c0 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sun, 25 Oct 2026 09:42:30 +0900 Subject: [PATCH 3/3] Describe GC APIs without emulator-specific terminology --- jvm/src/external_roots.rs | 2 +- jvm/src/jvm.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jvm/src/external_roots.rs b/jvm/src/external_roots.rs index 4d634fab..45ac8358 100644 --- a/jvm/src/external_roots.rs +++ b/jvm/src/external_roots.rs @@ -3,7 +3,7 @@ use alloc::vec::Vec; use crate::ClassInstance; /// Discovers roots in a consumer's execution state during synchronous collection. -/// Implementations must not call JVM APIs, run guest code, or mutate the object inventory. +/// Implementations must only inspect existing state, without calling JVM APIs or mutating the object inventory. pub trait ExternalRoots: Send + Sync { fn roots(&self, objects: &[&dyn ClassInstance]) -> anyhow::Result>; } diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 9345ce7d..fb154d2b 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -57,7 +57,7 @@ pub struct Jvm { inner: Arc, } -/// Roots host references until their result or exception has been published to the guest. +/// Keeps local references alive for the lifetime of a native frame. /// Scopes must be dropped in stack order, before detaching their originating thread. #[must_use] pub struct NativeFrame {