Monday, November 9, 2009

AS3 Memory Management

After developing in C++ for several years, I must admit I found the idea of garbage collection in higher-level languages like Java, C#, and ActionScript to be alluring.  Making sure every "new" was matched by a "delete" was tedious but absolutely necessary.  Like any serious C++ programmer, I developed patterns and habits for making sure the code I wrote was clean as I wrote it, because tracking down memory leaks after they've crept in is even more tedious than thinking about "delete" whenever I typed "new".

My first experience with a "garbage collected" language was C# (.NET).  After getting deep into development of a new product, I quickly discovered that the garbage collector was really not so magical, and not even really that smart (at least not for a highly memory-intensive application).  I learned that I had to think just as much about making sure memory was collected as expected and when needed as I did when writing C++.

When I first started with AS3, I figured I probably could trust the garbage collector this time (different language, different application, smaller scale), but as Now Boarding (our first AS3 project) neared completion, it became apparent that memory was again not so magically taken care of.  Really, I shouldn't have been so surprised, but I was somewhat disappointed, because my understanding of the garbage collector was that I shouldn't have had one of the problems I ended up facing in NB.

I'm writing this post as a practical guide to AS3 memory management.  You can read the technical specs and algorithms which the garbage collector uses elsewhere.  This guide is based on my many hours of experience with the memory profiler in Flex Builder 3 and is about what actually happens and what you can do to fix/avoid memory problems in AS3.

Now that the background is over, let's set the stage:

Why does memory management matter in AS3?

There are probably many smaller-scale games and applications out there in which the author thought basically nothing about memory management.  Sometimes that works.  For the larger-scale games we've made and are making, poor memory management manifests itself in a critically bad way: poor performance.  As the pool of allocated objects grows and grows, it takes the garbage collector longer and longer to process the references, and it has to run more frequently.  Each collection pass is a noticeable pause in the game which happens every few seconds.  The game also generally runs more and more slowly.  Eventually, the game becomes unplayable.

How does that happen?

The short answer is that the game is maintaining references to the objects that are no longer being used, and those references make the garbage collector (GC for short) think the objects are still being used.  The GC only frees memory allocated to an object if the object has nothing referencing it (or a group of objects that reference each other have nothing else referencing any of them).  If the game would only null out all references to an object, the GC would reclaim the memory, and the pool of objects would not keep growing.

Unfortunately, it's not quite that simple.  I tried to treat it like that, and it didn't work as I expected.  In NB, each game mode is an object that contain everything for that game mode.  The game mode object itself is the root of the whole object hierarchy.  I figured that if I just set the 1 reference to the game mode object to null, then the GC would recognize that the whole hierarchy was orphaned.  Nope!  Every game mode was staying in memory after the next one was loaded, so going in and out of the game made the memory go up every time until the game was unplayable.

In practice (maybe this is in the spec, I don't know, but I missed it if it was), there seems to be a limit on just how big an orphaned group of objects can be before the GC just gives up and assumes the whole group is still being used.  That brings us to my first tip:

TIP #1:  Explicitly null out your object references.  If you have an object hierarchy that you want to be collected, null out the references each object has to the other objects in that hierarchy explicitly.

Don't asume that the GC will recognize the whole thing is orphaned.

Along with nulling references, event listeners are also extremely "sticky" when it comes to making sure objects "stick" around.  In my experience, using weak references doesn't matter.  If one object (Object A) subscribes one of its methods to an event on another object (Object B), then Object A and Object B will never be collected, and any objects they reference will also never be collected.

TIP #2: For every call to addEventListener, make sure you call removeEventListener.

To fix the memory leak between game modes in NB, I had to make sure every single event listener was removed.  Each one that "leaked" caused the objects involved to stay in memory.  I give every object that references another object a destroy method that I make sure is called.  In that destroy method, I set all references to null (after calling destroy on all children that have a "destroy" method, of course), and I make sure all event listeners are removed.  Yes, that's obssessive-compulsive, but it entirely solves all memory leaks (well, unless you really are keeping references around...).

This is where the memory profiler in Flex Builder becomes indispensable.  Particularly useful, IMO, is the "Live Objects" view.  Running under the profiler, I can load a new game mode and see the object associated with the old game mode disappear (or not disappear, and then I can drill down and see what is still referencing that old game mode object).

TIP #3: Use the Flex Builder memory profiler.  It's only in the $699 version, but it has a 61-day trial, which is plenty of time to use it to fix your game.  Then just reinstall Windows when it comes time to work on your next game. ;)  If you're a serious AS3 developer, then the $699 price tag shouldn't deter you.  Get the tools you need!

Object Pooling

When an object isn't collected when the game is no longer using it, that's a memory leak, and if you have enough, then the game's memory usage will grow and grow until the game doesn't work any more.  That's a memory management issue AS3 developers need to be aware of.

Another potential problem that can frequently come up in various types of games is creating lots of objects while the game is running.  If the objects are leaking (i.e. not being collected), then the game's memory usage will grow as the player plays the game, leading to slow down and eventual crash (follow tips #1 and #2 to fix that).  If the objects aren't leaking (i.e. the memory is being reclaimed), but if the number or size of objects being created is large enough, then another problem happens: frequent pauses.

When the GC runs, it causes a noticeable pause in the game, as it collects all those properly orphaned objects.  If you watch the memory usage graph in the profiler, it will look like a saw blade.  Memory rises as those objects are created, then it drops back down suddenly every time the GC runs.  The more the GC has to collect when it runs, the longer it takes, and the greater the pause in the game (and the more jagged the saw blade).

If you have saw blade action in your memory usage graph, then you would most likely benefit from object pooling.  That's a fancy term that essentially means reusing your objects so that they never have to be collected.  Then your memory usage graph stays nice and flat, and the GC can basically just go to sleep while your game runs, never interrupting your code or the player's fun.

Once I saw the saw blade thing going on in NB, I tried to retrofit some object pooling into the game.  While I was able to drastically reduce the jaggedness of the graph, I couldn't pool everything (ran into some weird issues with one MovieClip), and so NB retains some saw blade action to this day.  I wrote some classes to facilitate object pooling which I used from the start in Clockwords, so CW doesn't have any saw blade action going on.  Its memory graph is a perfectly horizontal line, even though letters, explosions, hit animations, and bugs are continually being created and destroyed.  Then when the game is over, the memory usage drops back down as all objects in the old game mode are collected properly.

TIP #4: Pool objects that need to be created and destroyed frequently.

Object pooling is really simple.  I posted the class I wrote (along with a few helper classes which it uses, such as my linked list class) here.  There's no license or copyright or anything in the code, and you can do whatever you want to it or with it.

To use the gaObjectPool object, just create one and pass in the Class type you want to pool and a block size, which is just how many instances of Class the pool will instantiate if a caller wants an instance of Class, but there are no more left in the pool.  You can use 1 if you want (you can even make that a default value, since you have the code ;) ).

So once you create a gaObjectPool instance for the type you want to pool, stash the reference to that object wherever you need it.  Then when you want an instance of that type, call getObj, and cast the return value to your type.  Use the object as you please.

When you're done with the object, call the destroy method on it (you have one right?).  I find it useful as a design pattern to give all objects I pool a destroy method which resets all member variables to a known, default state, as if the object were just constructed.  That way when subsequent callers of getObj get the same instance back, they don't have to worry about what might be lingering in the reused object.  After calling destroy on the object you want to reuse, pass it in to the gaObjectPool.returnObj method.  That'll put the object back in its internal linked list so that the instance can be returned by a later call to getObj.

Last word of warning: don't return the same object instance to the pool more than once at a time.  That'll cause the object pool to hand it out more than once to different callers who then both try to use it for different things.

A quick note on gaList: it's a multi-purpose linked list class, which you probably noticed pools its node objects.  Linked lists are preferrable to Arrays when the contents of the list changes frequently.  In practice, most of my collections change frequently, so I use gaList for most of them.  For example, a queue (first in, first out) is a type of list that is perfectly suited to a linked list implementation.  To make a queue with gaList, use "add" to add objects to the end of the queue, then "removeHead" to remove and return the first item in the queue.  If you don't want to remove the head of the queue, then just use "getFirst" to peek at it.

Conclusion

If you're making a decent-sized game, you will probably run into some memory issues during the course of development (or...gasp...after release).  The issues might not be serious enough to refactor your code to retrofit these tips, but you should understand the potential pitfalls of AS3 memory management and know how to prevent them or at least fix them if they end up causing problems for you.