diff --git a/jvm/src/external_roots.rs b/jvm/src/external_roots.rs new file mode 100644 index 00000000..45ac8358 --- /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 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/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..fb154d2b 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, } +/// 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 { + 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.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(()) @@ -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(); @@ -1088,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, @@ -1135,15 +1180,38 @@ impl Jvm { self.inner.threads.read().len() } - // TODO we need safe, ergonomic api.. - pub fn push_native_frame(&self) { + pub fn push_native_frame(&self, args: &[JavaValue]) -> NativeFrame { let thread_id = (self.inner.get_current_thread_id)(); - self.inner.threads.write().get_mut(&thread_id).unwrap().push_native_frame(); + 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 pop_frame(&self) { + /// 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)(); - self.inner.threads.write().get_mut(&thread_id).unwrap().pop_frame(); + 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> { @@ -1186,36 +1254,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 +1389,6 @@ impl Jvm { } else { None }; - if let Some(object) = &synchronized_object { - self.monitor_enter(object).await?; - } - self.inner .threads .write() @@ -1332,33 +1396,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..5cdd4af1 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.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()]); + 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.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.push_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.push_native_frame(&[]); + JavaLangString::from_rust_string(&jvm, "argument").await? + }; + 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.push_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.push_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.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.push_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.push_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<()> { @@ -11,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()?; @@ -25,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()?; @@ -55,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( @@ -71,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()?; @@ -87,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?; @@ -107,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()?; @@ -124,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); @@ -150,15 +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?; + let frame = jvm.push_native_frame(&[]); + jvm.resolve_class("[Ljava/lang/Object;").await?; + drop(frame); jvm.collect_garbage()?; struct Object; - jvm.push_native_frame(); - let object: ClassInstanceRef = jvm.new_class("java/lang/Object", "()V", ()).await?.into(); + 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); @@ -178,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); @@ -213,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( @@ -241,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); @@ -278,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); @@ -296,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,)) @@ -313,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(()) @@ -326,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); 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 e9702cd0..5f0deb13 100644 Binary files a/test-data/MonitorSemantics$CounterWorker.class and b/test-data/MonitorSemantics$CounterWorker.class differ diff --git a/test-data/MonitorSemantics$FailingWorker.class b/test-data/MonitorSemantics$FailingWorker.class index c39937f7..35a0435a 100644 Binary files a/test-data/MonitorSemantics$FailingWorker.class and b/test-data/MonitorSemantics$FailingWorker.class differ diff --git a/test-data/MonitorSemantics.class b/test-data/MonitorSemantics.class index ea2b76b7..8ab4718b 100644 Binary files a/test-data/MonitorSemantics.class and b/test-data/MonitorSemantics.class differ diff --git a/test-data/ThreadInterruption.class b/test-data/ThreadInterruption.class index f5f00d78..b603b059 100644 Binary files a/test-data/ThreadInterruption.class and b/test-data/ThreadInterruption.class differ diff --git a/test-data/src/MonitorSemantics.java b/test-data/src/MonitorSemantics.java index 9dc5451d..3026d942 100644 --- a/test-data/src/MonitorSemantics.java +++ b/test-data/src/MonitorSemantics.java @@ -8,6 +8,7 @@ public void run() { synchronized (LOCK) { int current = counter; Thread.yield(); + System.gc(); counter = current + 1; } } @@ -24,6 +25,7 @@ public void run() { } private static synchronized void failWhileSynchronized() { + System.gc(); throw new RuntimeException("expected"); } diff --git a/test-data/src/ThreadInterruption.java b/test-data/src/ThreadInterruption.java index cc5ddbcf..323a132f 100644 --- a/test-data/src/ThreadInterruption.java +++ b/test-data/src/ThreadInterruption.java @@ -111,6 +111,7 @@ public static void main(String[] args) throws Exception { WAIT_LOCK.wait(); } } + System.gc(); waiter.interrupt(); synchronized (WAIT_LOCK) { while (!secondWaitReady) { diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index a0a2f410..f76ab4a4 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -24,9 +24,11 @@ use jvm_bytecode::{ArrayClassDefinitionImpl, ClassDefinitionError, ClassDefiniti use rustjava_runtime::{ File, FileDescriptorId, FileOpenOptions, FileSize, FileStat, FileType, IOError, IOResult, RT_RUSTJAR, Runtime, SpawnCallback, - get_bootstrap_class_loader, get_runtime_class_proto, + get_runtime_class_proto, }; +pub use rustjava_runtime::get_bootstrap_class_loader; + struct SpawnCallbacks { callbacks: Mutex>>, 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 {