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.

AttachmentSize
MemoryTracker.as4.84 KB
TestMemory.zip4.59 KB

Share:

Comments

okuligowski
Thu, 23/01/2014 - 12:13

Fantastic tool! Already helped a lot! Thanks!

Kaz Yaz
Wed, 23/11/2011 - 14:34

Hi there,

I just looked for your example fla and as files but they are not at the bottom of the post? Could you put them back on?

I tried using your code but I'm not sure what I need to do to get it to work, so your example would be useful so I can see how it works?

Also:

CONFIG::debug

and

CONFIG::release

come back as compile errors when I try to run it or debug it?

Damian Connolly
Wed, 23/11/2011 - 15:29

Hey Kaz Yaz,
Thanks for the heads up - I'm not sure why my post decided to delete my file listings. They're reattached now.

For the CONFIG::debug and CONFIG::release, these are conditional compilation checks, that you can read more about at http://divillysausages.com/blog/as3_conditional_compilation. Basically, when you see CONFIG::debug, that code will only be run if you're testing in debug mode. Code in CONFIG::release will only run in release mode. You're free to delete either if you can't get it to work (note that System.gc() will only work in debug mode).

If you code in FlashDevelop, these constants are defined automatically. If you code in flash, then the link above will show you how to add them. FlashBuilder should follow roughly the same conventions.

Kaz Yaz
Mon, 28/11/2011 - 16:33

Hi again,

thanks for this...

Please could you tell me how I attach the files to a fla so I can see it running? I'm not sure how the to .as files work. Do I need to make an .fla named MemoryTracker and then attach MemoryTracker.as as a class? What do I do with Main.as?

Sorry I'm a bit new to as3. I'm used to the old as2 and trying to convert over to as3.

Thanks

Damian Connolly
Mon, 28/11/2011 - 18:05

Hey Kaz,

If you take the code as it is, you just need to place the .as file in the same folder as your fla (as that's where it looks for code files by default), then make the calls as normal (either in your document class, or if you're coding from the timeline).

If you want to put the MemoryTracker into a subfolder, you'll need to update the package definition before referencing it. If you code using external .as files (as in setting the document class in the stage properties), then you'll need to call an import before you can use it (as far as I know, if you code in the timeline, all the imports are done automatically).

If you ping me an email using the contact form, I'll send you some examples this evening of how it's done

Kaz Yaz
Mon, 05/12/2011 - 12:27

thanks for your help. I managed to get it working. I've been moving over to as3 from as2 and starting to work out how classes work and how to package things that are in subfolders. Thanks for your help! Much appreciated! :)

Joe
Fri, 03/06/2011 - 07:00

it's cool!thanks :)

Newbie
Tue, 03/05/2011 - 21:34

Very good stuff here. I'm struggling to solve a memory leak in my first major project and this is giving me some good pointers. But could you explain your 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"?

Damian Connolly
Tue, 03/05/2011 - 23:48

Glad to hear the article is helping you!

What I mean by the "it doesn't delete the object itself" is that in AS3, we only manipulate references to objects in memory, not the objects itself. For example, if we take the line var s:Sprite = new Sprite;, what's actually happening here, is that we're instructing the Flash VM to grab some memory and use it to make a new Sprite, then assign a reference to that Sprite in the variable s. This creates a reference in the garbage collector. As far as it knows, a block of memory, X, has 1 reference to it (i.e. it's still being used).

When we then call s = null;, all we're actually doing is clearing that reference in the garbage collector. The memory that was allocated for that Sprite is still allocated. The garbage collector now knows that memory block X has no references. It still doesn't clear that memory however, until the garbage collector is run to collect up all the blocks of memory that have been dereferenced in our program. This happens when we explicitly call System.gc() in debug mode, or the memory requirements of our app increase (where the VM will try to reuse old memory rather than ask the OS for more - a slow process).

Newbie
Wed, 04/05/2011 - 20:23

Thanks, my real issue understanding what you meant was a reading problem. I didn't notice the comma after "in some cases" and misinterpreted your meaning.