Tracking memory leaks in AS3

If you're a super cool, handsome Flash dev like me, memory management is an important part of your daily coding routine. While Flash has a garbage collector available so you never have to go to the same lengths as in C++, there's a lot of things we can do to make its life a lot easier and make sure everything, including our game, runs like gravy. When things do go arse over tit and our program is leaking memory faster than my ability to forget names the minute they're told to me, how can we make life easier for ourselves? [Sod this, I only want the code]

Refresher time!

var s:Sprite = new Sprite;

What is s? If you say s is a Sprite then hang your head in shame. s is actually a reference to a Sprite. The difference is subtle but will help your understanding of how memory management in AS3 works. When editing this Sprite, you do it through it's reference, s.

In AS3, everything is passed as a reference (primitive objects like String, Number, int and their ilk have special operators in the background that make them act as if they were passed by value). If you don't know the difference between pass-by-reference and pass-by-value, think of it this way: when you pass an object to a function by reference, then you're passing the object itself; when you pass an object by value, then you're passing a copy of that object - any changes you do to it in the function won't make any difference when you return from the function.

Pass by value example:

var num:Number = 1.0;
this._foo( num );
trace( num ); // traces 1.0, as num is passed by value

private function _foo( n:Number ):void
{
	n++;
}

Pass by reference example:

var obj:Object = { num:1.0 };
this._foo( obj );
trace( obj.num ); // traces 2.0, as obj is passed by reference

private function _foo( o:Object ):void
{
	o.num++;
}

What's this got to do with memory management? Knowing where you're passing objects, and especially, where you're storing those objects, will help when you're wondering why an object is or isn't getting garbage collected.

How does the garbage collector work?

Magic.

Seriously though, the first thing you need to know is that we have no control over when the garbage collector runs. Unless you're in debug mode, where you can call System.gc(). Or you're in release mode, where you can use a dirty hack to call it (more on that later). For the most part, if you frown down on hacks (that attitude lasts until you're on the tightest of deadlines and your project needs to get shipped ;) ), you can't really tell the garbage collector to run. It'll run when your memory requirements increase, where it'll try to reclaim old memory rather than allocate new memory (memory is allocated in pages of about 1MB, so this is a good thing). When it does run, how does it know what to collect?

Reference counting

If we take some code like this:

var objA:Object = new Object;
var objB:Object = new Object;

objA.foo = objB;

And if we remember about references from the first part of this post, then we end up with something like this in the garbage collector:

A visual representation of reference counting in AS3

The garbage collector holds 3 references, 1 for objA and 2 for objB (setting the foo property of objA in the third line of code makes a new reference to objB). If we set objB to null, it will remove one reference (NOTE: calling myObj = null;, or delete myObj; in some cases, only clears the reference to the actual object; it doesn't delete the object itself). objB still won't be eligible for garbage collection though, as there's still a reference to it in the code. We need to call objA.foo = null; to clear the last reference and let it be collected. This is the most common cause of memory leaks: not nulling out all your references.

Mark & sweep

What happens if we change our code a bit to this:

var objA:Object = new Object;
var objB:Object = new Object;

objA.foo = objB;
objB.foo = objA;

We end up with something like this in the garbage collector:

A visual representation of circular referencing in AS3

Now we have two references for each Object. If we forget to null out the foo property on each object and instead just call:

objA = null;
objB = null;

We're left with a problem: there's still a reference to each object in the garbage collector (as each Object references the other in it's foo property, but we have no reference in our code to access the objects). Memory leak!

This is where the concept of mark & sweep comes in.

Flash traverses the SWF and marks every object that it can reach. This is the mark phase. In our example, objA and objB won't be marked as they're not reachable anywhere in the SWF (the only references to the objects are in each other). At the end of this phase, every object that's not marked is assumed to be dead and garbage collected (or swept). This takes care of that nasty circular reference problem and fixes our leak.

The main problem with this is that mark & sweep is sloooooooooooooooooooooooowwwww. It's performed incretementally, so if you're calling System.gc() to run the garbage collector yourself, you'll need to call it twice; once for the mark phase and again for the sweep phase.

If you've ever made a program and every so often it pauses for a split second, that's probably the garbage collector kicking in. Our goal as developers is to make sure this happens as little as possible. Ideally all collection would be through the reference counting method as that's the quickest. Actually, the ideal solution would be that the garbage collector would never run as it'd never feel the need to (remember, it's only called when our memory requirements keep increasing).

How to be a better developer

If you're new to AS3 memory management, there's a few tips, that if you keep at the front of your head when coding, will make your life a lot easier and will ensure that your game doesn't fall over on it's lardy, bloated face:

  • Event listeners: Either use weak listeners (myObj.addEventListener( Event.SUP, this._onEvent, false, 0, true );) or remove your listeners explicitly. Event listeners are set to be strongly linked by default, so if you want weak listeners, then you need to specify it. Using weak listeners means that the reference (as the event listener has a reference to your object) doesn't count with the garbage collector.
  • Clean up references: If objA has a link to objB, then when you set objB to null, it won't be eligible for collection as there's still a link to it. This is the most common cause of memory leaks.
  • Reuse objects: Instead of just nulling objects and throwing them at your feet (possible social commentary opportunity here...), reuse them. Create object pools and have reset() methods that return the object to it's original state.
  • Immediately kill objects: Certain objects, like XML and BitmapData can be immediately deleted, freeing up the memory that they held. BitmapData.dispose() and System.disposeXML() will do the job.
  • Know the language: Know what goes on in the background when you code. For example, setting cacheAsBitmap = true; on a DisplayObject is actually creating a bitmap of that object in the background. Rotating or scaling the DisplayObject means the bitmap has to be recreated. Ditto for filters. Calling BitmapData.rect actually creates a new Rectangle every time.
  • Beware loops: Loops and recursions are easy places to lose a lot of memory. Watch if you're creating a new object each loop and see if you can move the creation outside of the loop. E.g.:
    // creates 100 Point objects
    for( var i:int = 0; i < 100; i++ )
    {
    	var p:Point = new Point;
    	p.x = myObjs[i].x;
    	p.y = myObjs[i].y;
    	
    	// do something with p
    }
    vs.
    // creates 1 Point object
    var p:Point = new Point;
    for( var i:int = 0; i < 100; i++ )
    {
    	p.x = myObjs[i].x;
    	p.y = myObjs[i].y;
    	
    	// do something with p
    }

The MemoryTracker object

Fupping hell, this post is getting long.

A well known trick in AS3 is to use the weakKeys property of a Dictionary object to keep track of objects in memory. When a Dictionary is created with weakKeys = true;, then setting an object as a key doesn't increase it's reference count with the garbage collector. It's the same principle behind weak event listeners. It also means that we can use a Dictionary to see if any of our objects remain after they've been supposedly deleted. If they do remain, then we can add a label as a String to give us a clue as to where it's still being held (normally, depending on how many objects you track, you'll actually be left with 2 in memory, with one being responible for keeping the other there).

package  
{
	import flash.display.Stage;
	import flash.events.Event;
	import flash.net.LocalConnection;
	import flash.system.System;
	import flash.text.TextField;
	import flash.utils.Dictionary;
	import flash.utils.setTimeout;
	
	/**
	 * A tool for tracking objects in memory and seeing if they've been properly 
	 * deleted. The layout is described in 
	 * http://www.craftymind.com/2008/04/09/kick-starting-the-garbage-collector-in-actionscript-3-with-air/
	 * @author Damian Connolly
	 */
	public class MemoryTracker
	{
		
		/***********************************************************/
		
		private static var m_tracking:Dictionary = new Dictionary( true ); // the dictionary that we use to track everyone
		private static var m_count:int		= 0;			// we garbage collect over a few frames, so this keeps track of the frames
		private static var m_stage:Stage	= null;			// a reference to the stage - we need this for the frame listener
		private static var m_debug:TextField	= null;			// the debug textfield that we'll write to
		
		/***********************************************************/
		
		/**
		 * [write-only] A reference to the stage
		 */
		public static function set stage( s:Stage ):void
		{
			MemoryTracker.m_stage = s;
		}
		
		/**
		 * [write-only] Set a debug TextField to log into
		 */
		public static function set debugTextField( t:TextField ):void
		{
			MemoryTracker.m_debug = t;
		}
		
		/***********************************************************/
		
		/**
		 * Tracks an object
		 * @param obj	The object that we're tracking
		 * @param label	The label we want to associate with this
		 */
		public static function track( obj:*, label:String ):void
		{
			MemoryTracker.m_tracking[obj] = label;
		}
		
		/**
		 * Start garbage collection and check for any references that remain. This 
		 * will only work in the debug player or AIR app (as it's the only place we 
		 * can call System.gc()). This will work over a number of frames
		 */
		public static function gcAndCheck():void
		{
			// this can only work if we have the stage reference
			if ( MemoryTracker.m_stage == null )
			{
				MemoryTracker._log( "Please set the Stage reference in MemoryTracker.stage before calling this" );
				return;
			}
			
			// reset our count and start our enter frame listener
			MemoryTracker.m_count = 0;
			MemoryTracker.m_stage.addEventListener( Event.ENTER_FRAME, MemoryTracker._gc );
		}
		
		/***********************************************************/
		
		// Perform a garbage collect. We call it a number of times as the first time 
		// is for the mark phase, while the second call performs the sweep
		private static function _gc( e:Event ):void
		{
			// should we run the last collection?
			var runLast:Boolean = false;
			
			CONFIG::release
			{
				// dirty hack way of calling the garbage collector, so I'm
				// only running it on release mode
				try {
					new LocalConnection().connect( "foo" );
					new LocalConnection().connect( "foo" );
				} catch ( e:Error ) { }
				
				// just go direct to the last
				runLast = true;
			}
			
			CONFIG::debug
			{
				System.gc();
				
				// we run the last one if our count is right
				runLast = MemoryTracker.m_count++ > 1;
			}
			
			// should we stop the event listener and run the last gc?
			// In debug mode, we call System.gc() a total of 4 times: 3 in this 
			// function, and one final time in _doLastGC()
			if ( runLast )
			{
				// use e.target in the rare chance that MemoryTracker.stage has 
				// been cleared in the meantime
				( e.target as Stage ).removeEventListener( Event.ENTER_FRAME, MemoryTracker._gc );
				setTimeout( MemoryTracker._doLastGC, 40 );
			}
		}
		
		// Performs the last garbage collect. This is from the link at the top: 
		// "Lastly, not all features in AIR could be unhooked with our enterFrame 
		// trick, after another couple days of testing we found components that 
		// needed to be unhooked with Timers like the HTML component."
		private static function _doLastGC():void
		{
			CONFIG::debug
			{
				// only call this in debug mode
				System.gc();
			}
			
			// trace out the remaining objects in our dictionary
			MemoryTracker._log( "-------------------------------------------------" );
			MemoryTracker._log( "Remaining references in the MemoryTracker:" );
			for ( var key:Object in MemoryTracker.m_tracking )
				MemoryTracker._log( "  Found reference to " + key + ", label:'" + MemoryTracker.m_tracking[key] + "'" );
			MemoryTracker._log( "-------------------------------------------------" );
		}
		
		// traces out a message and logs it to our debug TextField if we have one
		private static function _log( msg:String ):void
		{
			trace( msg );
			if ( MemoryTracker.m_debug != null )
				MemoryTracker.m_debug.appendText( msg + "\n" );
		}
		
	}

}

The code behind the class is quite simple; there's only 2 functions exposed to the user. If we take the track() function, you just pass whatever object you want to track along with a label for it. This label could be a name to help identify it, or to provide a clue to it's location, or whatever you want really.

var s:Sprite = new Sprite;
MemoryTracker.track( s, "Sprite0");
MemoryTracker.track( s, "Sprite on the main stage");
MemoryTracker.track( s, "Sprite on the main stage + in myObjA + in myArray");

The second function gcAndCheck() starts the garbage collection process and at the end prints out all the objects that we still have in memory (you can pass a TextField to the MemoryTracker and it'll print out the messages there as well (for release mode)). The collection takes place over a number of frames as we need to let the garbage collection do it's thing (i.e., if you call System.gc() and immediately trace out the memory usage, there's no change. We need to wait until the next frame).

Multiple calls to System.gc() are also needed as the first call will invoke the mark phase, while the second will sweep up anything unlucky enough to have no friends.

You migh also notice the last call uses a timeout rather than an ENTER_FRAME listener. The guys over at CraftyMind found that:

...not all features in AIR could be unhooked with our enterFrame trick, after another couple days of testing we found components that needed to be unhooked with Timers like the HTML component.

One final thing you might notice in there, is that there's a few CONFIG::debug and CONFIG::release blocks floating around. This is Flash's equivalent of conditinal compilation. If you use FlashDevelop, then CONFIG::debug will be defined as true when you're in debug mode, while CONFIG::release will be true when you're in release mode. If not, you can either add them in yourself or just remove them from the code.

Why include them in the first place? I hear you ask. When you're in debug mode, you can call System.gc() to run the garbage collector. This doesn't work in release mode because, I don't know, Adobe doesn't like your face. There is a dirty hack that you can pull though; opening 2 LocalConnections in a try..catch block (needed as it'll throw an exception). This kickstarts the garbage collector. I put this under the conditional compilation as I'm a fan of standards whenever possible.

Bug time!

There's a bug with Flash and Sprites at the minute (there's a JIRA here if you want to vote on it). Basically if you have a Sprite with the buttonMode set to true, then when you click on the Sprite, Flash keeps a reference of it somewhere. Meaning, if you click on the Sprite, then null it, then run garbage collection, the Sprite will still exist in memory. To see an example of this, click on the Sprite, then run the garbage collection. Sprites with buttonMode set to false don't suffer this problem.

Some external links to give you more depth on the subject:

Example time!

You can download the example files at the bottom of the post.

Share: