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:

  1. The desire to use a Dictionary for quick lookups (e.g. compared to Vector)
  2. 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!