You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Commit 67267da attempted to fix the
JavaArrayTiming.ObjectArrayEnumerationTiming test by using
GC.Collect() to ensure that nothing was collected during enumeration.
This fix was DOOMED to failure, which is why it promptly failed again.
:-)
Instructive is [*why*][0] it was DOOMED. TL;DR: Finalizers *suck*, and
a "fully managed" cross-VM bridge implementation likely isn't
possible.
The setup: modify JavaObject to contain a ZombieLevel field, and
update the finalizer to update it:
partial class JavaObject {
public int ZombieLevel;
~JavaObject ()
{
ZombieLevel++;
// ...as before...
}
}
Then take the `foreach` loop in ObjectArrayEnumerationTiming, and
annotate:
foreach (var p in ps) {
var z = p.ZombieLevel;
var h = p.PeerReference;
Console.WriteLine ("# Original Zombie={0}; Current={1}", z, p.ZombieLevel);
using (var pt = new JniType (ref h, JniObjectReferenceOptions.Copy)) {}
}
The result is that *sometimes* the ZombieLevel's differ, so here's
what's happening: JavaObject has a finalizer, so:
1. JavaObject instance is constructed
2. Instance from (1) is eventually eligible for collection (no roots)
and placed on the finalization queue.
3. JavaObject finalizer is run for (1), which sees that the
corresponding Java instance is still alive, calls
GC.ReRegisterForFinalize().
4. Instance from (1), *while still alive*, is *also* still on the
finalization queue!
5. The GC thread continues to run the finalizer, ~constantly, until
the instance stops re-registering itself.
(3), (4), and (5) are the killers here: while the GC is (usually) a
stop-the-world collector, finalizer *execution* does not require that
the world be stopped, and are thus executed "normally" by the
finalizer thread, *in parallel* with normal code execution!
This can introduce significant failures: `p` is thus *shared* between
multiple threads -- the thread running the test, and the finalizer
thread, attempting to finalize `p` -- and finalizer execution will
*change* p.PeerReference, *invalidating* other copies like `h`!
This is Horrifically Bad™.
There are only two fixes here:
1. Don't use finalizers, or
2. Don't use finalizers by themselves.
Which brings us to a question: Why's this work in Xamarin.Android?
Java.Lang.Object has a finalizer, why doesn't this break ALL THE TIME?
The answer is taht Xamarin.Android follows (2): it uses Mono's
embedding API to use a custom GC bridge via
`mono_gc_register_bridge_callbacks()`, and this GC bridge ensures that
the Java.Lang.Object finalizer WILL NOT EXECUTE until the
corresponding Java instance is also collected.
Which means Xamarin.Android doesn't experience the scenario of a
Java.Lang.Object instance on the finalization queue constantly having
its finalizer executed concurrently with "normal" code, so things work
(more or less) as expected.
The fix here? Skip JavaObject entirely here, and stick with the
lower-level JniObjectReference API. No objects, no finalizers, no
problem.
The *proper* long-term fix? Java.Interop with Xamarin.Android will use
Xamarin.Android's GC bridge.
Java.Interop with Desktop Mono is another matter; we either find a way
to remove the JavaObject finalizer (yet somehow make it optional on
Xamarin.Android?), or we copy Xamarin.Android'sx GC bridge.
Copying the Xamarin.Android GC bridge works, but means we require a
mono runtime environment; things won't work -- cannot work? -- on
desktop .NET.
Which raises a closesly related question: is it even *possible* to
have a fully-managed VM+GC bridge implementation that doesn't involve
using Mono's GC bridge API, something portable to e.g. CoreCLR?
At present, that doesn't look to be possible.
[0]: https://xamarinhq.slack.com/archives/D03DZPU5V/p1448469633000024
0 commit comments