Skip to content

[release/11.0.1xx] [Java.Interop] Release native peer control blocks on finalization - #13294

Merged
jonathanpeppers merged 6 commits into
release/11.0.1xxfrom
backport/pr-13016-to-release/11.0.1xx
Oct 28, 2026
Merged

jonathanpeppers merged 6 commits into
release/11.0.1xxfrom
backport/pr-13016-to-release/11.0.1xx

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Backport of #13016 to release/11.0.1xx

/cc @jonathanpeppers @simonrozsival

simonrozsival and others added 5 commits October 28, 2026 13:22
Preserve the value-manager callback ordering and release cleared JavaObject and JavaException control blocks after Dispose(false), including exceptional callbacks. Keep references reconstructed by derived callbacks alive. Cover normal, invalid, partial-construction, resurrection, and explicit-disposal lifetimes separately from JNI reference counts.

Refs #13010

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Share the finalization validity guard between JavaObject and JavaException without changing callback or reference-tracking context ordering.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep native allocation and callback checks in the ordinary shared-VM suite. Move exact GREF-count assertions to the dedicated JniReferenceLeak selection and warm peer caches after collection, so unrelated runtime activity does not mask the lifetime regression checks.

Refs #13010

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Remove the retired JavaVMFixture inheritance and host-only registered-peer cleanup. Current main removed the reflection-backed JVM harness; the shared fixture now follows its no-base Android convention and exercises the actual bridge finalizer path.

Refs #13010

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Exercise successful and throwing finalization callbacks for both peer types after valid reconstruction. Verify retained JNI identity and registration, then explicit disposal releases the native block and removes registration.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Initialize the JVM for focused fixture runs and keep host finalizer test peers out of the strong-reference registry. Preserve Android registration behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers

Copy link
Copy Markdown
Member

@dalexsoto review

@jonathanpeppers

Copy link
Copy Markdown
Member

Verified this PR with the actual MaintainerHandoff_AndroidPeerCleanup_20261023.zip sample. The unfixed .NET 11 source reproduces continuing native memory growth, and this fix removes that growth for this sample on CoreCLR/x86_64. This is a separate runtime comparison from the Mono test recorded on #13293; it does not establish the sample's original Mono behavior.

Source and test settings

Android source revision Java.Interop runtime version label
Unfixed 75d7a7283cf6476fec8d3c2ba767aac092be06fd git-rev-head:75d7a7283c
Fixed f4dbe071dacdff50aedf9db7f218ed2ba5da4b79 git-rev-head:f4dbe071da

The unfixed revision is the original release/11.0.1xx PR base. Both real source SDK/framework builds and private local workloads succeeded. The sample used each checkout's dotnet-local.cmd, not the handoff's prebuilt APKs or rewritten framework DLLs. Both source checkouts remained clean.

The comparison used the same owned Android 10/API 29 x86_64 emulator, CoreCLR/.NET 11.0.0, Android workload pack version 37.2.1, and JDK 17. The source SDK/framework configuration was Debug; the sample was Release, with trimming, ReadyToRun, the interpreter, and marshal methods disabled.

The private SDK reports 11.0.100, commit 50580101e1, from package 11.0.100-rtm.26524.110. An attempted Mono sample build failed in that SDK with:

NETSDK1242: Building Android projects with the Mono runtime is not supported in .NET 11.0 and later. Use the CoreCLR runtime or target .NET 10.0.

This is the downloaded SDK's restriction, not a conclusion that the release-branch source contains no Mono runtime code. Both measured variants use CoreCLR.

Sample configuration changes were the SDK pin 10.0.402 -> 11.0.100, app/helper target frameworks from .NET 10 to .NET 11, and RID android-arm64 -> android-x64. UseMonoRuntime=false was explicit. RunAOTCompilation=false was replaced with PublishReadyToRun=false, as required by the CoreCLR build. The collector's APK/ADB paths and server port were adjusted for the owned emulator. The allocation workload and collection/finalization steps were unchanged; MainActivity.cs matches the archive byte-for-byte.

Measurements

First completed the original four cases: finalization and explicit Dispose for each source version, each with 2,000 warm-up allocations and 20 batches of 5,000 Android.Graphics.Rect wrappers. Those short runs were too noisy for a firm conclusion: native growth over batches 0-20 was +132,712 bytes for unfixed finalization versus +1,587,024 for its disposal control.

Then repeated all four cases with the sample's supported -Batches 100 setting: 500,000 measured wrappers per fresh app process, with the same 2,000-allocation warm-up. All eight runs completed without a reported failure or process restart. Each extended run contains all 101 measured checkpoints, including batch 0, and 600 generation-2 collections between batches 0 and 100. Framework instrumentation was disabled throughout.

All changes below are in bytes. Batch 0 is after warm-up; batches 50-100 cover 250,000 new wrappers.

Version / case Native change, 0-100 Native change, 5-100 Native change, 50-100 Managed live change, 50-100 Java live change, 50-100
Unfixed, finalization +7,045,800 +5,481,456 +4,244,856 0 0
Unfixed, explicit Dispose -529,736 -1,904,256 +112,200 -16,408 0
Fixed, finalization -1,208,672 -2,133,528 -19,608 0 0
Fixed, explicit Dispose -512,064 -1,734,408 +210,968 0 0

In the last 250,000 allocations, unfixed finalization added 16.979 native bytes per wrapper, while fixed finalization changed by -0.078 bytes per wrapper. These are endpoint changes divided by allocation count, not median per-batch changes. Managed and Java live memory stayed unchanged in both finalization runs during that interval.

The unfixed finalization increase exceeded its disposal control by 4,132,656 bytes in that window, or about 16.531 bytes per wrapper. That is consistent with the .NET 11 control-block payload's 16-byte size on x64, rather than the 32-byte reference measurements from the .NET 10 sample. The size comparison is supporting evidence, not an exact count of leaked blocks.

Both disposal controls and fixed finalization lack the continuing increase seen with unfixed finalization. Remaining positive/negative changes include background allocation and allocator/cache cleanup; a negative delta is not a negative control-block count or proof of a permanent memory plateau.

Framework identity checks and limits

Decoded the managed framework payloads from each actual signed APK. Java.Interop.dll and Mono.Android.dll match their respective source-built runtime-pack files byte-for-byte. Runtime START records identify Java.Interop 75d7a7283c / f4dbe071da and Mono.Android 75d7a72 / f4dbe07 for unfixed/fixed runs. Inspection of the packaged finalizer IL confirms FreeIfInvalid is absent from the unfixed finalizer and present in the fixed finalizer.

The sample/helper DLLs, System.Private.CoreLib.dll, and native libcoreclr.so hashes are identical between versions. The installed fixed APK was also pulled back from the emulator and matched the signed build APK. The native type-map module-ID cross-check described in the .NET 10 comment was not performed here.

Stock mode leaves the optional probe counters at zero. These results measure aggregate native allocation growth after the sample's finalizer drains, with live-heap observations and disposal controls; they are not direct per-block allocation/free or finalization-event counts.

This verifies removal of the sample's continuing native growth on this .NET 11 CoreCLR/x86_64 configuration. Mono and ARM64 hardware were not verified, and this does not establish that all native memory growth in a larger app is fixed. Exact commands, build logs/binlogs, signed APKs, DLL hashes, and all eight measurement captures were retained locally.

@jonathanpeppers

Copy link
Copy Markdown
Member

@dalexsoto stamp

@dalexsoto dalexsoto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Stamping as requested by @jonathanpeppers; no code review performed.

@jonathanpeppers

Copy link
Copy Markdown
Member

Checked the failures in Azure build 1652683, which tested PR head f4dbe071dacdff50aedf9db7f218ed2ba5da4b79. The current failures appear unrelated to the peer-control-block changes. The build remains failed: 40 jobs passed and 2 failed.

Failed job Evidence Assessment
macOS MSBuild+Emulator 4 Set emulator started flag failed before tests ran. PowerShell crashed at startup with Call to 'procargs' failed with errno 5 and Microsoft.PowerShell.ManagedPSEntry+StartupException. CI host failure, not a failure in the changed code.
macOS MSBuild+Emulator 1 InstallAndRunTests.DotNetRunWithDeviceParameter failed both initially and on automatic retry. The assertion at InstallAndRunTests.cs:731 did not find the expected device-target text for emulator-5570. Appears to be a separate CLI/test issue, not peer finalization.

For the device-parameter failure, inspected the attached dotnet-run-device-output.log and device logcat. The saved output shows a successful DeployToDevice build with zero warnings/errors, but no expected launcher/device-target output. The device log shows package installation without an app launch or native crash. This does not identify the exact CLI cause, but provides no evidence of a regression in the changed finalization code.

The test-results page also retains an earlier MAUI CoreCLR/ARM64 Full R2R failure. Its Helix console shows APK installation failed with INSTALL_FAILED_INSUFFICIENT_STORAGE before the app could run. The later run passed both checks, and the MAUI GitHub check is green.

The Java.Interop host suite now passes on both Windows and macOS: 748 passed, 5 skipped on each. The four ActualFinalizer_ReleasesControlBlock failures addressed by the test-only correction are no longer present.

@jonathanpeppers
jonathanpeppers merged commit d5c897c into release/11.0.1xx Oct 28, 2026
41 of 44 checks passed
@jonathanpeppers
jonathanpeppers deleted the backport/pr-13016-to-release/11.0.1xx branch October 28, 2026 16:58
jonathanpeppers pushed a commit that referenced this pull request Oct 28, 2026
Backport only the unregistered-disposal correction from c663cd1
(#12772), without its peer-identity and activation changes.

DisposePeerUnlessReferenced() bypasses Disposed()/Dispose(true) when it
calls the private reference-deletion helper.  JavaObject and
JavaException then retain their native control blocks after the JNI
reference is cleared and finalization is suppressed.  Use the public
DisposePeer(value) path so managed disposal precedes normal cleanup.

Add a distinct JavaVMFixture-based fixture for this branch's host JVM
and Android inclusion paths.  Cover valid-reference callback entry,
native-block release, registered no-ops, alias winners, throwing
callbacks and retry, repeated disposal, exact Rect disposal, and
separate warmed global-reference deletion checks.

The source-built Release SDK passes 12 host-JVM tests and 9 ordinary
plus 7 dedicated GREF tests on each of CoreCLR and NativeAOT.  The
original helper produces exactly 6 host and 7 device failures; restore
the production correction before repeating positive validation.

Complement the finalization backport in #13294 without duplicating
its implementation or JavaPeerControlBlockTests fixture.

Refs #13290, #13292, #13294

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 97270e0d-9fba-4a8b-8a04-9b7c25e1cd21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants