Cross Platform
Android
iOS
Mac

Garbage Collection

Xamarin.Android uses Mono's Simple Generational garbage collector. This is a garbage collector with two generations, and thus two kinds of collections: minor collections and major collections.

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 android.app.Activity 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. 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.

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 http://bugzilla.xamarin.com/show_bug.cgi?id=1084#c6.

Note: Be extremely careful when disposing of Java.Lang.Object subclasses. If the subclass is known to be a Java type, disposing is fine, but if the subclass is actually a managed subclass, e.g. a custom Activity, then disposing of the instance will break the mapping, and the next time the Java instance is surfaced in managed code a new Managed Callable Wrapper will be instantiated, which will have lost any instance data that the original instance contained.

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 GC.Collect(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:

Diagnostics

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

Configuration

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.

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:

MONO_GC_PARAMS=soft-heap-limit=128m