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:
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:
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 toobjB
, then when you setobjB
tonull
, 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
andBitmapData
can be immediately deleted, freeing up the memory that they held.BitmapData.dispose()
andSystem.disposeXML()
will do the job. - Know the language: Know what goes on in the background when you code. For example, setting
cacheAsBitmap = true;
on aDisplayObject
is actually creating a bitmap of that object in the background. Rotating or scaling theDisplayObject
means the bitmap has to be recreated. Ditto forfilters
. CallingBitmapData.rect
actually creates a newRectangle
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.:
vs.
// 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 }
// 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 ourenterFrame
trick, after another couple days of testing we found components that needed to be unhooked withTimers
like theHTML
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 LocalConnection
s 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 Sprite
s 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. Sprite
s with buttonMode
set to false
don't suffer this problem.
Links
Some external links to give you more depth on the subject:
- An excellent write-up on GC in Flash by Michelle Yaiser at Adobe: http://www.adobe.com/devnet/actionscript/learning/as3-fundamentals/garbage-collection.html
- A really in-depth look at what's going on behind the scenes by J.P. AuClair: http://jpauclair.net/2009/12/23/tamarin-part-iii-current-garbage-collector-in-flash-10-0/
Example time!
You can download the example files at the bottom of the post.
Comments
Here are some link to get deeper info on the GC
http://jpauclair.net/2009/12/23/tamarin-part-iii-current-garbage-collector-in-flash-10-0/
And here is a profiler that might help you track Memory issue:
http://jpauclair.net/flashpreloadprofiler/
Thanks Jean-Philippe! I'm a big fan of your work :D
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"?
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 callSystem.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).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.
it's cool!thanks :)
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?
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
andCONFIG::release
, these are conditional compilation checks, that you can read more about at https://divillysausages.com/2011/04/04/as3-conditional-compilation-or-ifdef-in-flash/. Basically, when you seeCONFIG::debug
, that code will only be run if you're testing in debug mode. Code inCONFIG::release
will only run in release mode. You're free to delete either if you can't get it to work (note thatSystem.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.
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
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
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! :)
Fantastic tool! Already helped a lot! Thanks!
Amazing article, very well explained and very helpful. Thanks you!
Great article, thank you!
Submit a comment