Dictionary Memory Leak
The Dictionary
class provides perhaps the most useful support for weak references—and therefore garbage collection control—in the AS3 Flash API. However, due to a subtle error in its documentation, you may inadvertently be leaking a lot of memory. Today’s article shows you how this can happen and how you can easily fix the leak.
Adobe’s documentation for weakKeys
currently states:
weakKeys:Boolean (default = false) — Instructs the Dictionary object to use “weak” references on object keys. If the only reference to an object is in the specified Dictionary object, the key is eligible for garbage collection and is removed from the table when the object is collected.
However, Philippe recently commented that this might not be entirely accurate. If a Dictionary
key’s value is the key itself, the reference to the object is still “in the specified Dictionary object”. However, the key-value pair will not be removed from the Dictionary
, nor will it be garbage collected.
The documentation should state this: (change emphasized)
weakKeys:Boolean (default = false) — Instructs the Dictionary object to use “weak” references on object keys. If the only reference to an object is the object key itself, the key is eligible for garbage collection and is removed from the table when the object is collected.
To test out this difference, I have made a small test app that simply builds a Dictionary
in two ways:
// First way: // key == 1 MB BitmapData // value == key dict = new Dictionary(true); for (var i:int; i < SIZE; ++i) { var key:BitmapData = new BitmapData(512, 512, true); dict[key] = key; } // Second way: // key == 1 MB BitmapData // value == some other object (the Boolean true) dict = new Dictionary(true); for (var i:int; i < SIZE; ++i) { var key:BitmapData = new BitmapData(512, 512, true); dict[key] = true; }
In both cases, the key is “weakly referenced” (“weak” for short) because nothing else references it but the Dictionary
. That is, I simply discard the local variable and never save it to a field for later.
Try out the test app below. Notice that as you scale up the size of the Dictionary
with “Value==Key”, the total memory usage and count of key-value pairs both grow larger and larger as the key-value pairs are not being garbage collected. Then try out “Value==Other” and notice that memory stays flat and the key-value pair count remains at zero regardless of size because all of the key-value pairs are being collected almost immediately by the garbage collector.
package { import flash.display.*; import flash.system.*; import flash.events.*; import flash.utils.*; import flash.text.*; [SWF(width=300,height=120,backgroundColor=0xEEEAD9)] public class DictionaryMemoryLeak extends Sprite { private static const TEXT_FORMAT:TextFormat = new TextFormat("_sans", 11); private static const TYPE_KEY:String = "Value==Key"; private static const TYPE_OTHER:String = "Value==Other"; private var memory:TextField = new TextField(); private var keyValuePairs:TextField = new TextField(); private var dict:Dictionary; private var typeFrame:Sprite; private var typeChoice:String; private var sizeFrame:Sprite; private var sizeChoice:int; public function DictionaryMemoryLeak() { stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; this.typeFrame = addChoices( "Dictionary Type: ", onTypeButton, 0, [TYPE_KEY, TYPE_OTHER] ); this.sizeFrame = addChoices( "Dictionary Size: ", onSizeButton, this.typeFrame.height, ["10", "50", "100"] ); this.typeChoice = TYPE_KEY; this.sizeChoice = 10; refill(); this.memory.autoSize = TextFieldAutoSize.LEFT; this.memory.y = this.sizeFrame.y + this.sizeFrame.height; this.memory.text = "Gathering memory usage..."; addChild(this.memory); this.keyValuePairs.autoSize = TextFieldAutoSize.LEFT; this.keyValuePairs.y = this.memory.y + this.memory.height; this.keyValuePairs.text = "Gathering key-value pairs..."; addChild(this.keyValuePairs); var about:TextField = new TextField(); about.defaultTextFormat = TEXT_FORMAT; about.htmlText = "Dictionary Memory Leak Demo " + "by <font color=\"#0071BB\"><a href=\"" + "http://jacksondunstan.com/articles/1190" + "\">JacksonDunstan.com</a></font>"; about.autoSize = TextFieldAutoSize.LEFT; about.selectable = false; about.y = this.keyValuePairs.y + this.keyValuePairs.height; addChild(about); addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function addChoices( promptStr:String, callback:Function, y:Number, choices:Array ): Sprite { var prompt:TextField = new TextField(); prompt.defaultTextFormat = TEXT_FORMAT; prompt.text = promptStr; prompt.autoSize = TextFieldAutoSize.LEFT; prompt.selectable = false; addChild(prompt); var x:Number = prompt.width; const PAD:Number = 3; for each (var choiceName:String in choices) { var tf:TextField = new TextField(); tf.defaultTextFormat = TEXT_FORMAT; tf.name = "label"; tf.text = choiceName; tf.autoSize = TextFieldAutoSize.LEFT; tf.selectable = false; tf.x = tf.y = PAD; var button:Sprite = new Sprite(); button.name = choiceName; button.graphics.beginFill(0xE6E2D1); button.graphics.drawRect(0, 0, tf.width+PAD*2, tf.height+PAD*2); button.graphics.endFill(); button.addChild(tf); button.addEventListener( MouseEvent.CLICK, function(ev:MouseEvent): void { var button:Sprite = ev.currentTarget as Sprite; var label:TextField = button.getChildByName("label") as TextField; callback(label.text); } ); button.x = x; button.y = y; addChild(button); x += button.width + PAD; } prompt.y = y + (button.height - prompt.height) / 2; var frame:Sprite = new Sprite(); frame.graphics.lineStyle(1); frame.graphics.drawRect(0, 0, tf.width+PAD*2, tf.height+PAD*2); addChild(frame); return frame; } private function onTypeButton(label:String): void { this.typeChoice = label; refill(); } private function onSizeButton(label:String): void { this.sizeChoice = int(label); refill(); } private function refill(): void { var typeButton:Sprite = getChildByName(this.typeChoice) as Sprite; this.typeFrame.x = typeButton.x; this.typeFrame.y = typeButton.y; this.typeFrame.width = typeButton.width; this.typeFrame.height = typeButton.height; var sizeButton:Sprite = getChildByName(String(this.sizeChoice)) as Sprite; this.sizeFrame.x = sizeButton.x; this.sizeFrame.y = sizeButton.y; this.sizeFrame.width = sizeButton.width; this.sizeFrame.height = sizeButton.height; this.dict = new Dictionary(true); for (var i:int; i < this.sizeChoice; ++i) { var key:BitmapData = new BitmapData(512, 512, true); this.dict[key] = this.typeChoice == TYPE_KEY ? key : true; } } private function onEnterFrame(ev:Event): void { var mem:uint = System.totalMemory; var memMB:Number = mem / (1024.0*1024.0); this.memory.text = "Memory: " + mem + " (" + memMB.toFixed(2) + "MB)"; var count:uint; for (var k:* in this.dict) { count++; } this.keyValuePairs.text = "Key-Value Pairs Remaining: " + count; } } }
The memory leak I’ve just described can occur easily. It even happened to me in the Fast AS3 MultiMap article I wrote a month ago where Philippe tipped me off to this problem. The root cause is usually because of a combination of two factors:
- The desire to use a
Dictionary
for quick lookups (e.g. compared toVector
) - The lack of a value to match to the key you want to look up
Since you have to have a value, it’s tempting to just use the key again. However, this defeats the “weak keys” feature and triggers the memory leak. So, in conclusion, I recommend that you use a simple literal like true
or 123
as a placeholder value. You just might save a lot of memory!
Spot a bug in the demo app? Know any more quirks with the garbage collector or Dictionary
? Post a comment!
#1 by de on August 29th, 2011 ·
When you do
dict[key] = key;;
there are two references to key spawned. For the key, AND for the value. One is weak, one is not. So the documentation is correct.
#2 by jackson on August 29th, 2011 ·
Yes, but both references are “in the specified Dictionary object”, so the documentation says they should be collected. At least that’s how I read it.
#3 by jpauclair on August 29th, 2011 ·
lol, it’s not a memory leak! It’s a feature!
Still, great post name.. got me hooked ;)
#4 by jackson on August 29th, 2011 ·
I suppose it could be considered a feature. :)
In the way I used the term “memory leak”, I meant that it is a “memory leak that can happen with Dictionaries” and not a memory leak in
Dictionary
itself.#5 by Guyllaume on August 29th, 2011 ·
It’s that kind of little quirk that makes me wish the AS3 Doc was a wiki. The way it’s written right now is really not the way it behaves.
#6 by Brian Rinaldi on August 29th, 2011 ·
I agree with @de that the doc is technically correct. However, I have actually sent along your wording suggestion to the doc team. If a slight rewording or otherwise might prevent a common mistake, then it’s worth looking into.
#7 by Zach on August 29th, 2011 ·
Something about this post reminds me of someone complaining that a screwdriver doesn’t make a very good hammer. The logic of storing the object itself as the value of the dictionary – when in order to access that value, you need the key, but the key is also the value… Who the heck would do that? I’m glad you included the thought process by which you came up with that. Very interesting post – keep up the good work. And Keep up the original non-linear thinking too.
#8 by jackson on August 29th, 2011 ·
You’re exactly right about this: I’m looking for a set data structure and AS3 isn’t providing one that’s built-in. A HashSet would be great, but it’s just not there. I could implement one myself, but it would be orders of magnitude slower than just using
Dictionary
, bloat up my SWF, and waste my time. So, I’ll use (abuse?)Dictionary
(which is a map/associative array) and just leave off the value. :-/#9 by Michael on September 8th, 2011 ·
You are stupid.
1.weak references is key not value
2.BitmapData will call dispose to distroy
#10 by jackson on September 8th, 2011 ·
You are rude. :)
1. This wasn’t in dispute. Please read Adobe’s documentation that I cited in the article. It says “If the only reference to an object is in the specified Dictionary object, the key is eligible for garbage collection and is removed from the table when the object is collected.” Since the value is in the
Dictionary
, it should be removed per the documentation. The article shows that it is not.2. For one, you don’t need to call
dispose
to destroy aBitmapData
. Flash will free up its memory just fine if it is garbage collected. Second,BitmapData
was just a convenient class to allocate a lot of memory.#11 by Spoky on September 25th, 2011 ·
Ok, Dictionary hold a reference to a local variable instance. The local variable instance is not garbage collected due to the reference, so the weak key can not be garbage collected too.
Does local variable cause a
performance drop when it trigger the garbage collection? Will a member variable improve the performance? Thanks for the thought.
#12 by jackson on September 25th, 2011 ·
Let me see if I understand the two scenarios right. The “local variable” scenario looks like this:
In that case,
bigMemory
goes out of scope at the end of the constructor function and there are no longer any references to it except the weak key in__dict
, so it is collected.The second scenario goes like this:
In that case,
__bigMemory
never goes out of scope (as long as theExample
instance is still referenced, of course) so the garbage collector is never triggered.To me, these aren’t directly comparable situations. In the former, you are freeing up a big chunk of memory and clearing out a key-value pair from a
Dictionary
. In the latter, you’re taking steps to prevent this by explicitly holding a reference to that chunk of memory. In the end, it’s up to what you want to do: free the memory or not.As for performance, garbage collection will take some time and is definitely not a “fast” operation like doing a few arithmetic operators. The trick is to avoid it while still not using “too much” memory, however you define that.
#13 by Good on September 3rd, 2012 ·
got interesting results with method keys
instances of A, B and C wasn’t collected, but D, E and F was removed
it means that value that points to key itself or it’s part prevents collecting, in other cases it works like a charm
here is my test:
#14 by jackson on September 3rd, 2012 ·
Nice test and explanation! One thing I’ll add to clarify is that methods, represented by the
Function
type, necessarily contain a reference to the instance of the class they run on. This means that a method key also points to the instance, which makes all (A-F) of the tests effectively store thethis
pointer in the key and A-D store thethis
pointer in the value, which is the subject of the article (leak when the value is the weak key) but not E and F.#15 by Good on September 4th, 2012 ·
Yes D is the most interesting case
A method closure is generated when some method is referenced. The method closure automatically remember its original object instance.
But it doesn’t prevent garbage collection, at least in FP 11.1.102 (haven’t tested in older players)
as far as I know there was a bug before with this method closure keys, but now it looks like fixed
http://gskinner.com/blog/archives/2006/07/as3_dictionary_.html
http://mikecann.co.uk/personal-project/as3-dictionary-weak-method-closures/
#16 by Good on September 4th, 2012 ·
No bug is there
method closure key is removed while method owner and method are not removed
#17 by jackson on September 4th, 2012 ·
Good to hear. After all, it has been six years. :)
#18 by Russ on September 14th, 2012 ·
I’m confused why this thread is saying it is fixed. From what I see, the bug still exists in the latest player (11.4). I would describe the bug as “methods can/should NEVER be used as weak keys because they are immediately eligible for garbage collection”.
Here’s my proof script (written to also check non-debug players), which checks functions, closures, and methods for use as weak keys:
On any player I test this yields:
ie: The method key gets garbage collected, even though the method’s parent still exists. This is the bug.
Am I missing something?
#19 by jackson on September 14th, 2012 ·
What you’re missing is a test where the value associated with your keys is the key itself. The leak being reported in the article is this:
Not this:
For more, check out the example with
BitmapData
in the article as well as the demo app source (click Show Demo App Source Code to see it). The only relation methods, functions, instances and such have with this is that they’re yet-another kind of object that you could use as a key and value and, particularly, one that can “hide” another reference.#20 by Good on September 4th, 2012 ·
Yes D is the most interesting case
A method closure is generated when some method is referenced. The method closure automatically remember its original object instance.
But it doesn’t prevent garbage collection, at least in FP 11.1.102 (haven’t tested in older players)
as far as I know there was a bug before with this method closure keys, but now it looks like fixed
#21 by ybemiqukwu on April 9th, 2013 ·
tfhxykbdltpoevotubo, yaxzlnbkor , [url=http://www.lxosnbtwil.com]qozvurjgbo[/url], http://www.bkmofioygr.com yaxzlnbkor
#22 by Phen375 on April 9th, 2013 ·
xvhookbdltpoevotubo, Phen375, FschXQM, [url=http://phen375reviewblog.com/]Phen375[/url], DwHqWMB, http://phen375reviewblog.com/ Phen 375, ypYUicM.