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.

25 comments:

  1. Thanks a lot for sharing your experience and knowledge. It saves time on experiments.
    We too have created a big-sized game, and right now it is ready for release, but we just found out of the memory leakage problem because of not nullifying the objects (and non-primitive standard objects) - because the orphaned objects do not get garbage collecdted by themselves.
    By the way, I was amazed when I found this post by typing ing google search a really long line: "as3 flash destroying one object which contains references to other objects destroy" :)
    P.S.: Living in a rural environment is great!
    I'll pass a link to the game in a few days to you ;)
    Cheers!

    ReplyDelete
  2. Hi, Flash, glad it was helpful. If anything is unclear or you have any more questions, just ask....

    Go google search!

    ReplyDelete
  3. Thanks so much for this post, Tom. It's saved my bacon. http://www.andymoore.ca/2010/03/motherfucking-as3-garbage-collection/

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. My trick is to avoid creating anything, and if you do, make sure it's under carefully managed circumstances. Automate your dereferencing if possible, nulling things out is boring. Keep your listeners weak too.

    I've never liked pooling either, except in certain situations. It's not a particularly good solution, and more often just more complexity and mystery bugs.

    Coincidentally, my games run really fast and don't get slower as they're played.

    ReplyDelete
  6. Thanks Tom,
    a nice detailed refresher...
    each object I use implements an interface which has recycle() and harakiri().
    They all reference to static UMem, which manages everything related to memory management...
    Also if you subset classes, you ca use a static utility that knows how to destroy each kind of object, so in other classes you can just call Umem.kill(object)
    and it will take care of all necessary steps...
    Also, it will pool them so that once they are used they can be recycle()d.
    It pretty much depends on the kind of object...
    There are many ways to do this but I think theoretically the first rule to keep in mind is to create a framework, that you will ALWAYS use, no matter how boring, and stick to that.
    Each object should be able to be:
    - instantiated
    - initialized
    - recycled
    - disposed
    Separate instantiation from initialization and reuse...
    Memory will not be a problem...
    wisdom
    Filippo

    ReplyDelete
  7. We have been working on a tiny lib for Event Management that does a lot more for managing your events called Event Controller. it has a few bonus features like event logging and clustering. Take a look at http://fla.as/ec we'd love feedback!

    ReplyDelete
  8. @Max: Yeah, sometimes you need to create stuff. Pooling isn't the best solution for every case, but it is a good solution any time you do need to create stuff repeatedly. The objects you pool need to be designed to support it.

    @PippoMusic: That sounds like a good way to handle it. Designing each object with reusability in mind prevents the weird bugs that can crop up with object pooling. I agree that the most important thing is always using the framework. Leaks happen precisely because we miss things here and there.

    @corl3an: That event library looks pretty cool. Based on my own coding patterns, the ability to just remove all events from an object would be awesome. Usually I create an object, add all the listeners I want, and then remove them all in a destroy method. Every time I add a new one, I have to add a call to remove it elsewhere in the code. It would be nice to just have 1 method call to remove everything!

    ReplyDelete
  9. Thanks Tom! It has been really nice for us to. We just recently added the support for removing with RegEx which takes the removing to the next level. The log is great to since you can view all active events and sort through them easily as well. Thanks for the feedback we just open sourced the lib about 2 weeks about so its very new too.

    ReplyDelete
  10. If you assign a new object to a variable is the old object garbage collected? I always thought it was, but after reading this article I'm unsure.

    For example:

    var a = new FancyObject();
    ...
    a = new FancyObject();

    Will the first FancyObject be garbage collected, or should I set a to null before re-assigning?

    ReplyDelete
  11. RichardG, as far as I know, the first instance of FancyObject should get collected. I haven't had any problems with that scenario or ones like it, e.g. I don't null a local variable that holds a reference, because it'll go away when the function exits.

    ReplyDelete
  12. Ah, thank you for that. Interesting!

    ReplyDelete
  13. Thank you very much Tom, for sharing this kind of info. I'm new in the AS3 language, which i use for development of web pages. Maybe the memory usage of a web page is not so critical as the one in videogames, however it is important to know such things especially when programing flash galleries with lots and lots of images floating there and making funny effects. By some time i went looking for some info about the Garbage Collector, and even with that knowlegde i always got memory leaks, but now with all this info you gave i think my programs will be out of that problem. Thank you once again. Would you mind if i reference this post in a blog of my company?. And by the way, out of the topic here, if is not a problem, could you be so kind to provide me some references i can read to introduce me in games development if related to as3 better, since is the language i'm using by now.

    ReplyDelete
  14. Hi, Luis, I'm glad this post was helpful. Go ahead and reference it if you want, that's what the internet is for! All the things I've read about introductory game development I read before I got into Flash, so I don't know of any for AS3. I'm sure there are though! Gamedev.net is a great place to get started on game development in general. I'm old enough that I still read books, and I definitely recommend picking up any highly-rated book from Amazon on beginning game development.

    ReplyDelete
  15. This is a brilliant post :)
    Thanks for sharing.

    ReplyDelete
  16. Hi all, has anyone had the problem of using the YouTube AS3 API in an application to find that its destroy() method doesn't have quite the desired effect? Can anyone comment?

    ReplyDelete
  17. thanks for this post. However, i'm wondering if this has the same effect as using the:

    var a = new Object();

    delete(a);

    method? :) thanks tom!

    ReplyDelete
  18. @Daniel: I haven't used the YouTube API.

    @kimjks2003: The delete operator only works with dynamic properties of dynamic classes. In your example, delete would do nothing. But if you had created a property on that object after allocating it, then you could delete the property, like this:

    a.name = "test";
    delete a.name;

    That would remove the property "name" from the object "a".

    -Tom

    ReplyDelete
  19. This post has solved a lot of headaches, thanks for sharing!

    ReplyDelete
  20. Just started to adapt to AS, this was a really great post about the GC. Thank you very much.

    ReplyDelete
  21. "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:"

    Have you proven this in a test project (other than your game)? Seems like it should be simple enough to prove, but my test project so far seems to suggest that GC is doing the right thing.

    ReplyDelete
  22. PS. Perhaps you should try updating your "Filters" to increase the number of back-reference paths. By default there is a limit of 10, but you can select "Show All Back-Reference Paths".

    ReplyDelete
  23. @Brian: I didn't create a separate test project. I used the Flash Builder memory profiler to measure the before and after in my game. It's also possible that Adobe has fixed that issue. They've made a lot of changes to the Flash player in the last few years.

    ReplyDelete
  24. I also created a generalized object pool that provides you with control over the access and creation strategies, you can get more detail on my blog: http://bit.ly/Rfc6Ed

    ReplyDelete
  25. I think a game has a "glaring design problem" doesn't mean it really has a problem or that it won't sell
    Cool flash games

    ReplyDelete