Garbage Collection

PDF for offline use:

Xamarin.Android uses Mono's Simple Generational garbage collector. This is a mark-and-sweep garbage collector with two generations and a large object space, with kinds of collections: minor collections (collects Gen0 heap) and major collections (collects Gen1 and large object space heaps).

Note: In the absense of an explicit collection via GC.Collect() collections are on demand, based upon heap allocations. This is not a reference counting system; objects will not be collected as soon as there are no outstanding references, or when a scope has exited. The GC will run when the minor heap has run out of memory for new allocations. If there are no allocations, it will not run.

Minor collections are cheap and frequent, and are used to collect recently allocated and dead objects. Minor collections are performed after every few MB of allocated objects. Minor collections may be manually performed by calling GC.Collect(0)

Major collections are expensive and less frequent, and are used to reclaim all dead objects. Major collections are performed once memory is exhausted for the current heap size (before resizing the heap). Major collections may be manually performed by calling GC.Collect() or GC.Collect(GC.MaxGeneration).

Cross-VM Object Collections

There are three categories of object types.

  • Managed objects : types which do not inherit from Java.Lang.Object , e.g. System.String . These are collected normally by the GC.
  • Java objects : Java types which are present within the Dalvik VM but not exposed to the Mono VM. These are boring, and won't be discussed further. These are collected normally by the Dalvik VM.
  • Peer objects : types which implement IJavaObject , e.g. all Java.Lang.Object and Java.Lang.Throwable subclasses. Instances of these types have two "halfs" a managed peer and a native peer . The managed peer is an instance of the C# class. The native peer is an instance of a Java class within the Dalvik VM, and the C# IJavaObject.Handle property contains a JNI global reference to the native peer.

There are two types of native peers:

  • Framework peers : "Normal" Java types which know nothing of Xamarin.Android, e.g. android.content.Context .
  • User peers : Android Callable Wrappers which are generated at build time for each Java.Lang.Object subclass present within the application.

As there are two VMs within a Xamarin.Android process, there are two types of garbage collections: Dalvik collections and Mono collections. Dalvik collections operate normally, with caveat: a JNI global reference is treated as a GC root. Consequently, if there is a JNI global reference holding onto a Dalvik VM object, the object cannot be collected, even if it's otherwise eligible for collection.

Mono collections are where the fun happens. Managed objects are collected normally. Peer objects are collected by performing the following process:

  1. All Peer objects eligible for Mono collection have their JNI global reference replaced with a JNI weak global reference.
  2. A Dalvik VM GC is invoked. Any Native peer instance may be collected.
  3. The JNI weak global references created in (1) are checked. If the weak reference has been collected, then the Peer object is collected. If the weak reference has not been collected, then the weak reference is replaced with a JNI global reference and the Peer object is not collected. Note: on API 14+, this means that the value returned from IJavaObject.Handle may change after a GC.

The end result of all this is that an instance of a Peer object will live as long as it is referenced by either managed code (e.g. stored in a static variable) or referenced by Java code. Furthermore, the lifetime of Native peers will be extended beyond what they would otherwise live, as the Native peer won't be collectible until both the Native peer and the Managed peer are collectible.

Object Cycles

Peer objects are logically present within both the Dalvik and Mono VM's. For example, an Android.App.Activity managed peer instance will have a corresponding framework peer Java instance. All objects that inherit from Java.Lang.Object can be expected to have representations within both VMs.

All objects that have representation in both VMs will have lifetimes which are extended compared to objects which are present only within a single VM, such as a System.Collections.Generic.List<int>. Calling GC.Collect won't necessarily collect these objects, as the Xamarin.Android GC needs to ensure that the object isn't referenced by either VM before collecting it.

To shorten object lifetime, Java.Lang.Object.Dispose() should be invoked. This will manually "sever" the connection on the object between the two VMs by freeing the global reference, thus allowing the objects to be collected faster.

Automatic Collections

Starting in Release 4.1.0, Xamarin.Android will automatically perform a full GC when a gref threshold is crossed. This threshold is 90% of the known maximum grefs for the platform: 1800 grefs on the emulator (2000 max), and 46800 grefs on hardware (maximum 52000). Note: Xamarin.Android only counts the grefs created by , and will not know about any other grefs created in the process. This is a heuristic only.

When an automatic collection is performed, a message similar to the following will be printed to the debug log:

I/monodroid-gc(PID): 46800 outstanding GREFs. Performing a full GC!

The occurrence of this is non-deterministic, and may happen at inopportune times (e.g. in the middle of graphics rendering). If you see this message, you may want to perform an explicit collection elsewhere, or you may want to try to reduce the lifetime of peer objects.

GC Bridge Options

Xamarin.Android offers transparent memory management with Android and Dalvik. It is implemented as an extension to the Mono garbage collector called the GC Bridge.

The GC Bridge works during a Mono garbage collection and figures out which peer objects needs their "liveness" verified with the Dalvik heap. It does so by inducing the mono reference graph of unreachable peer objects into the Java objects they represent, then performing a Java GC and, finally, verifying which objects are really dead.

This complicated process is what enables subclasses of Java.Lang.Object to freely reference any objects and removes any restrictions on what Java objects can be bound to C#.Because of this complexity, the bridge process can be very expensive and cause noticeable pauses in an application. If the application is experiencing significant pauses it's worth investigating one of the following three GC Bridge implementations:

  • Old - The default implementation and is considered the most stable of the three. This is the bridge that an application should use if the GC_BRIDGE pauses are acceptable.
  • New - A major overhaul of the original code, fixing two instances of quadratic behavior but keeping the core algorithm, based on Kosaraju's algorithm for finding strongly connected components.
  • Tarjan - A completely new design of the GC Bridge based on Robert Tarjan's algorithm and backwards reference propagation . It does perform the best under our simulated workloads but has the larger share of experimental code.

The only way to figure out which GC Bridge is the best by experimenting in an application and analyzing the output. There are two ways to collect the data for benchmarking:

  • Enable logging - Enable logging (as describe in the Configuration section) for each GC Bridge option, then capture and compare the log outputs from each setting. Inspect the GC messages for each option, the GC_BRIDGE messages in particular. Pauses up to 150ms for-non interactive applications are tolerable, pauses above 60ms for very interactive applications (such as games) are a problem.
  • Enable bridge accounting - Bridge accounting will display the average cost of the objects pointed by each object involved in the bridge process. Sorting this information by size will provide hints as to what is holding the largest amount of extra objects.

To specify which GC_BRIDGE option an application should us, pass bridge-implementation=old, bridge-implementation=new or bridge-implementation=tarjan to the MONO_GC_PARAMS enviroment variable, for example:


Helping the GC

There are multiple ways to help the GC to reduce memory use and collection times.

Disposing of Peer instances

The GC has an incomplete view of the process, and may not run when memory is low because the GC doesn't know that memory is low.

For example, an instance of a Java.Lang.Object type or derived type is at least 20 bytes in size (subject to change without notice, etc., etc.). Managed Callable Wrappers don't add any additional instance members, so when you have a Android.Graphics.Bitmap instance which refers to a 10MB blob of memory, Xamarin.Android's GC won't know that -- the GC will see a 20 byte object and can't determine that it's linked to a Dalvik-allocated object that's keeping 10MB of memory alive.

It is frequently necessary to help the GC. Unfortunately, GC.AddMemoryPressure() and GC.RemoveMemoryPressure() are not supported, so if you know that you just freed a large Java-allocated object graph you may need to manually call GC.Collect() to prompt a GC to release the Java-side memory, or you can explicitly dispose of Java.Lang.Object subclasses, breaking the mapping between the managed callable wrapper and the Java instance. For example, see

Note: Be extremely careful when disposing of Java.Lang.Object subclass instances.

If the Java or managed instance may be shared between multiple threads, it should not be Dispose()d, ever.

For example, Typeface.Create() may return a cached instance. If multiple threads provide the same arguments, they will obtain the same instance. Consequently, Dispose()ing of the Typeface instance from one thread may invalidate other threads, which can result in ArgumentExceptions from JNIEnv.CallVoidMethod() (among others) because the instance was disposed from another thread.

If the instance refers to a type which isn't a binding of a Java type, such as a custom Activity, DO NOT call Dispose() unless you know that no Java code will call overridden methods on that instance. Failure to do so results in NotSupportedExceptions.

For example, if you have a custom click listener:

partial class MyClickListener : Java.Lang.Object, View.IOnClickListener {
    // ...

You should not dispose of this instance, as Java will attempt to invoke methods on it in the future:

Button b = FindViewById<Button> (Resource.Id.myButton);
using (var listener = new MyClickListener ())
    b.SetOnClickListener (listener);

If the instance is of a bound Java type, the instance can be disposed of as long as the instance won't be reused from managed code and the Java instance can't be shared amongst threads (see previous Typeface.Create() discussion). (Making this determination may be difficult.) The next time the Java instance enters managed code, a new wrapper will be created for it.

This is frequently useful when it comes to Drawables and other resource-heavy instances:

using (var d = Drawable.CreateFromPath ("path/to/filename"))
    imageView.SetImageDrawable (d);

The above is safe because the Peer that Drawable.CreateFromPath() returns will refer to a Framework peer, not a User peer. The Dispose() call at the end of the using block will break the relationship between the managed Drawable and framework Drawable instances, allowing the Java instance to be collected as soon as Dalvik needs to. This would not be safe if Peer instance referred to a User peer; here we're using "external" information to know that the Drawable cannot refer to a User peer, and thus the Dispose() call is safe.

Reduce Referenced Instances

Whenever an instance of a Java.Lang.Object type or subclass is scanned during the GC, the entire object graph that the instance refers to must also be scanned. The object graph is the set of object instances that the "root instance" refers to, plus everything referenced by what the root instance refers to, recursively.

Consider the following class:

class BadActivity : Activity {

    private List<string> strings;

    protected override void OnCreate (Bundle bundle)
        base.OnCreate (bundle);

        strings.Value = new List<string> (
                Enumerable.Range (0, 10000)
                .Select(v => new string ('x', v % 1000)));

When BadActivity is constructed, the object graph will contain 10004 instances (1x BadActivity, 1x strings, 1x string[] held by strings, 10000x string instances), all of which will need to be scanned whenever the BadActivity instance is scanned.

This can have detrimental impacts on your collection times, resulting in increased GC pause times.

You can help the GC by reducing the size of object graphs which are rooted by User peer instances. In the above example, this can be done by moving BadActivity.strings into a separate class which doesn't inherit from Java.Lang.Object:

class HiddenReference<T> {

    static Dictionary<int, T> table = new Dictionary<int, T> ();
    static int idgen = 0;

    int id;

    public HiddenReference ()
        lock (table) {
            id = idgen ++;

    ~HiddenReference ()
        lock (table) {
            table.Remove (id);

    public T Value {
        get { lock (table) { return table [id]; } }
        set { lock (table) { table [id] = value; } }

class BetterActivity : Activity {

    HiddenReference<List<string>> strings = new HiddenReference<List<string>>();

    protected override void OnCreate (Bundle bundle)
        base.OnCreate (bundle);

        strings.Value = new List<string> (
                Enumerable.Range (0, 10000)
                .Select(v => new string ('x', v % 1000)));

Minor Collections

Minor collections may be manually performed by calling <a href= "">GC.Collect</a>(0). Minor collections are cheap (when compared to major collections), but do have a significant fixed cost, so you don't want to trigger them too often, and should have a pause time of a few milliseconds.

If your application has a "duty cycle" in which the same thing is done over and over, it may be advisable to manually peform a minor collection once the duty cycle has ended. Example duty cycles include:

  • The rendering cycle of a single game frame.
  • The whole interaction with a given app dialog (opening, filling, closing)
  • A group of network requests to refresh/sync app data.

Major Collections

Major collections may be manually performed by calling GC.Collect() or GC.Collect(GC.MaxGeneration).

They should be performed rarely, and may have a pause time of a second on an Android-style device when collecting a 512MB heap.

Major collections should only be manually invoked, if ever:


To track when global references are created and destroyed, you can set the debug.mono.log system property to contain gref and/or gc.


The Xamarin.Android garbage collector can be configured by setting the MONO_GC_PARAMS environment variable. ( Environment variables may be set with a Build action of AndroidEnvironment.)

The MONO_GC_PARAMS environment variable is a comma-separated list of the following parameters:

  • nursery-size = size : Sets the size of the nursery. The size is specified in bytes and must be a power of two. The suffixes k , m and g can be used to specify kilo-, mega- and gigabytes, respectively. The nursery is the first generation (of two). A larger nursery will usually speed up the program but will obviously use more memory. The default nursery size 512 kb.
  • soft-heap-limit = size : The target maximum managed memory consumption for the app. When memory use is below the specified value, the GC is optimized for execution time (fewer collections). Above this limit, the GC is optimized for memory usage (more collections).
  • evacuation-threshold = threshold : Sets the evacuation threshold in percent. The value must be an integer in the range 0 to 100. The default is 66. If the sweep phase of the collection finds that the occupancy of a specific heap block type is less than this percentage, it will do a copying collection for that block type in the next major collection, thereby restoring occupancy to close to 100 percent. A value of 0 turns evacuation off.
  • bridge-implementation = bridge implementation : This will set the GC Bridge option to help address GC performance issues. There are three possible values: old , new , tarjan

For example, to configure the GC to have a heap size limit of 128MB, add a new file to your Project with a Build action of AndroidEnvironment with the contents: