Skip to content

Investigation: defer ART collection in the CoreCLR GC bridge #12436

Description

@simonrozsival

Summary

This issue records an experiment evaluating whether the CoreCLR Android GC bridge can avoid synchronously invoking java.lang.Runtime.gc() and instead defer Java peer reclamation until ART runs collection naturally.

The result is promising but not yet production-ready:

  • Deferred collection worked for an 80-minute stress run containing approximately 349,000 Android Callable Wrappers (ACWs).
  • ART naturally reclaimed JNI weak global references (WREFs), allowing the corresponding managed handles to be retired.
  • The WREF table remained below ART's 51,200-entry limit.
  • Avoiding synchronous Runtime.gc() reduced typical bridge time and improved frame pacing in a separate rendering benchmark.
  • The trade-offs were higher memory use, larger retained WREF populations, and worse bridge-time outliers.

This is an investigation report, not yet a proposal to change the product implementation.

Related context:

Prototype design

The prototype changes the bridge lifecycle as follows:

  1. Unreachable Java peers are converted from JNI strong global references (GREFs) to WREFs.
  2. The bridge completes without calling Runtime.gc().
  3. WREFs remain registered across subsequent bridge rounds.
  4. Later rounds probe each WREF using NewLocalRef().
  5. If ART cleared the WREF, the WREF is deleted and its managed handle is released.
  6. Live WREFs are temporarily strengthened only when needed to rebuild the current bridge graph.

A process-wide sweep handles WREFs that are no longer part of the current CoreCLR cross-reference graph.

For comparison, an adb property can enable the old behavior before each sweep:

debug.net.gcbridge.explicit_gc=0  # natural ART collection
debug.net.gcbridge.explicit_gc=1  # invoke Runtime.gc()

The property is read during every bridge round, so both modes use the same APK and runtime implementation.

Stress workload

The test application repeatedly:

  • creates a random component containing 1–1,000 custom ACWs;
  • connects the component into a strongly connected ring with additional random edges;
  • waits 10 ms between individual ACW allocations;
  • drops the only root after completing the component;
  • records managed GC counts, managed heap data, Java heap data, JNI GREF count, RSS, bridge population, reclamation, and phase timings.

Both configurations ran for 10, 20, 40, and finally 80 minutes on the same host-accelerated arm64 emulator using its default ART heap policy. All runs completed without a WREF overflow or fatal runtime error.

80-minute A/B result

Metric Natural ART collection Explicit Runtime.gc()
ACWs allocated 348,659 347,124
Bridge rounds 134 147
Maximum WREFs 35,510 7,616
Final WREFs 27,052 2,437
Handles reclaimed 321,609 343,266
Median bridge algorithm time 9.9 ms 17.3 ms
95th-percentile bridge algorithm time 45.8 ms 21.3 ms
Maximum bridge algorithm time 77.4 ms 22.6 ms
Median Runtime.gc() time 12.8 ms
Final managed heap size 73.5 MiB 8.8 MiB
Final RSS 255.0 MiB 182.1 MiB

Throughput was intentionally paced by the test and was effectively identical.

Natural collection produced a repeating sawtooth pattern: WREFs accumulated and then fell when ART independently collected their Java referents. The bridge observed 21 reclamation cycles during the natural 80-minute run.

80-minute natural ART timeline

80-minute natural versus explicit ART collection

Natural ART collection is marked at the bridge round that first observed and reclaimed cleared WREFs. Explicit collection timestamps come directly from ART's Explicit concurrent mark compact GC log entries.

Rendering benchmark result

A separate five-minute host-GPU emulator comparison used the application from runtime#131370:

Metric Natural ART collection Explicit Runtime.gc()
Average bridge time 3.1 ms 11.3 ms
Maximum bridge time 18 ms 37 ms
Frames below 55 FPS 2 7
Maximum WREFs 2,465 641

This indicates that removing synchronous ART collection can provide a meaningful typical-latency and frame-pacing improvement. A physical Samsung S23 test also confirmed that ART can naturally clear deferred bridge WREFs under a real 60 FPS workload, although collection was infrequent when Java heap pressure was low.

What we learned

The lifecycle is technically feasible

Keeping peers as WREFs across bridge rounds works. ART can clear them naturally, and the bridge can later detect that state and safely retire the associated managed handles.

It removes a significant synchronous cost

Runtime.gc() directly adds ART collection time to the bridge operation. Avoiding it improved typical bridge duration in both the stress workload and the rendering benchmark.

It exchanges pause time for memory and tail latency

Without explicit collection, more WREFs and managed contexts remain registered. Process-wide probing becomes more expensive as that population grows. The natural run therefore had a better median but substantially worse outliers and higher RSS.

The current diagnostics deliberately scan every registered reference and emit detailed logcat records. They contribute to measured latency, so production measurements without this instrumentation should be better. The underlying sweep is still proportional to the retained WREF population, however.

Natural ART scheduling is not a safety guarantee

ART collected often enough in this stress workload, but applications with little Java allocation pressure can retain WREFs for much longer. The prototype cannot guarantee that ART will collect before its fixed 51,200-entry WREF table fills.

Requirements before considering a product change

  • Provide a hard overflow-prevention strategy instead of relying exclusively on ART scheduling.
  • Make WREF cleanup incremental or otherwise bound the work performed during one bridge round.
  • Validate resurrection and peer-reconnection behavior.
  • Repeat realistic application and physical-device measurements without detailed diagnostic scanning/logging.
  • Evaluate memory and frame-time percentiles across a wider range of workloads and ART implementations.

Current conclusion

Deferred ART collection is worth pursuing. It successfully moves expensive Java collection out of the normal GC bridge path and improves typical bridge latency and frame pacing.

It is not yet safe to ship as-is because reclamation timing is controlled by ART, memory retention is higher, and the current design has no guaranteed protection against WREF-table exhaustion.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: CoreCLRIssues that only occur when using CoreCLR.Area: PerformanceIssues with performance.needs-triageIssues that need to be assigned.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions