Skip to content

[Mono.Android] fix global ref leak in TypeManager.Activate#11112

Draft
jonathanpeppers wants to merge 3 commits intomainfrom
dev/peppers/fix-activate-gref-leak
Draft

[Mono.Android] fix global ref leak in TypeManager.Activate#11112
jonathanpeppers wants to merge 3 commits intomainfrom
dev/peppers/fix-activate-gref-leak

Conversation

@jonathanpeppers
Copy link
Copy Markdown
Member

Fixes: #11101
Fixes: #10989

Context: 5c23bcda8 (PR #9640)

The Java.Interop Unification (5c23bcd) changed Object to extend JavaObject, introducing an additional ConstructPeer call in the constructor chain. TypeManager.Activate was updated to use SetPeerReference but never set the Activatable state flag. Without it, each ConstructPeer call in the constructor chain (JavaObject() and Object.SetHandle()) creates a new JNI global ref, overwriting the previous one without deleting it — leaking 3 global refs per LayoutInflater.Inflate call.

Before the fix, the new test fails with:

Global reference leak detected: 30 extra global refs after
inflating/GC'ing 10 custom views. Before=207, After=237

This went unnoticed because the Activate path is only triggered when Java creates .NET objects (not the other way around). The two main scenarios are Activity recreation and custom C# views in Android XML layouts. Most developers use .NET MAUI, which has a single Activity and does not use custom C# views in Android layout XML files, so neither scenario was commonly hit.

Changes:

  • Promote GetUninitializedObject from a local function in CreateProxy to a shared static method. It sets Activatable | Replaceable, which tells ConstructPeer to return early and not create duplicate global refs.

  • Have Activate call GetUninitializedObject then ConstructPeer to create one global ref while jobject is still a valid JNI local ref. The Activatable flag then prevents duplicates during the constructor chain.

  • Add regression test that inflates custom views and asserts JNI global reference count does not grow.

Copilot AI review requested due to automatic review settings April 14, 2026 19:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a JNI global reference leak when Java activates managed peers via TypeManager.Activate, preventing repeated ConstructPeer calls in constructor chains from creating duplicate global refs (notably impacting LayoutInflater.Inflate with custom C# views).

Changes:

  • Update TypeManager.Activate to create an uninitialized peer with Activatable|Replaceable state and use ValueManager.ConstructPeer for correct peer construction/registration.
  • Promote GetUninitializedObject to a shared helper so both activation and proxy creation consistently set the required managed peer state.
  • Add a regression test that inflates a custom view repeatedly and asserts JNI global ref counts remain stable.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/Mono.Android/Java.Interop/TypeManager.cs Fixes activation path to construct peers correctly and avoid duplicate global refs during constructor chaining.
tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/CustomWidgetTests.cs Adds regression coverage to detect global ref growth when inflating custom views from XML.

Comment thread tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/CustomWidgetTests.cs Outdated
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good fix for TypeManager.Activate. We need to apply the same fix to JavaMarshalValueManage.ActivateViaReflection (CoreCLR) and SimpleValueManager.ActivateViaReflection (and possibly other implementations of value managers we have for various purposes) in follow-up PRs.

Comment thread src/Mono.Android/Java.Interop/TypeManager.cs Outdated
@simonrozsival
Copy link
Copy Markdown
Member

The new test works well, it shows the memory leak on CoreCLR and Native AOT:

Mono.Android.NET_Tests-CoreCLR
Mono.Android.NET_Tests, Xamarin.Android.RuntimeTests.CustomWidgetTests.InflateCustomView_ShouldNotLeakGlobalRefs / Release

Global reference leak detected: 56 extra global refs after inflating/GC'ing 10 custom views. Before=142, After=198
Expected: True
But was:  False
Mono.Android.NET_Tests, Xamarin.Android.RuntimeTests.CustomWidgetTests.InflateCustomView_ShouldNotLeakGlobalRefs / Release

Global reference leak detected: 71 extra global refs after inflating/GC'ing 10 custom views. Before=125, After=196
Expected: True
But was:  False

Maybe we should apply the fix to all the "VMs" already in this PR

Fixes: #11101
Fixes: #10989

The fix is in Java.Interop (ConstructPeer + Dispose for Invalid refs).
This commit bumps the submodule and adds an on-device regression test
that inflates custom views and asserts no global ref leak.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers jonathanpeppers force-pushed the dev/peppers/fix-activate-gref-leak branch from c64fdb9 to 31a44fa Compare April 15, 2026 14:08
@jonathanpeppers jonathanpeppers marked this pull request as draft April 15, 2026 14:09
jonathanpeppers and others added 2 commits April 15, 2026 13:50
Round 1 acts as warmup, letting background threads settle.
Round 2 measures with a clean baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GlobalReferenceCount is process-wide and affected by Android system
services, GC bridge processing, and finalizer threads. Using 100
inflations makes a real leak (300+ delta) far exceed any background
noise, while a threshold of 100 tolerates device variability.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants