In part one, we looked at some simple memory management in C, and why we need to be careful with what we do with our memory. Now, we’ll take a look at some more advanced topics, including garbage collectors, reference counting and automatic memory management.
Our previous rule works great for simple programs, but there is a problem. Because we’ve defined a single owner for a piece of memory, our memory is only valid during the lifetime of its owner. Consider the following example:
To solve this problem, we have to duplicate memory for use by other objects via a Copy, to ensure that memory is not deallocated before we’re done. This is OK for small objects, but when we’re having to duplicate large objects, we can quickly run out of memory.
Furthermore, if we decide to have Object2 make a copy of “Some Memory”, to ensure it stays around while we need it, then Object1 modifies the original, those changes won’t appear in Object2’s copy. Depending on the program, this may or may not be what we want, but it’s starting to get complicated. How can we make sure we’ve always got access to the memory we want, even if the object that created it is long gone?
As programming languages and practices have evolved, two main solutions to this problem have arisen. One is Reference Counting, used in languages such as Objective-C (when developing on iOS), which is what we’ll cover first. The other is the concept of a Garbage Collector, which is used in languages like C# and Java (and Objective-C when developing on OSX). Both concepts revolve around the idea that instead of being gready and having one owner for a piece of memory, we can in fact have many owners. Most of the principles talked about here can be applied to any language, but most of the more modern have been designed around one or the other of these memory management techniques.
The idea with reference counting is that, along with the memory itself, we store a number of how many people currently own it (i.e. a count of how many references there are to the object). Every time a new owner wants to use the memory, we add 1 to that number. When someone is done with it, we subtract 1. When the number is at 0, we know there are no more owners, so we free up the memory. Take the following example:
- John buys a car.
- The car now has 1 owner – (John)
- Jimmy likes the car and, because everyone is nice in this world, is allowed to become an owner too.
- The car now has 2 owners – (John & Jimmy)
- John no longer wants the car, as he has now died.
- The car now has 1 owner (Jimmy)
- Jimmy decides he is done with the car, and relinquishes his ownership.
- The car now has 0 owners, and so gets taken to the scrap yard and is recycled.
Great! We successfully shared a car between two people and cleaned up after ourselves when we were done. No more old, unwanted cars filling up the streets, and even better, we didn’t destroy the car with Jimmy still inside!
So, how do we use this system? Well, it’s actually pretty straight forward. All we need to do is store our reference count (i.e. how many people own the memory) along with the memory itself. This part comes down to which language you’re using. Remember we said previously that some languages were designed with reference counting in mind? Well, Objective-C is one of those languages. Every object in Objective-C stores its reference count (called a retain count in Objective-C) in the object itself. For other languages, you can have a few options. C++, for example, doesn’t currently have any inbuilt support for reference counting, so your options are either to roll your own solution, or to use an automatic one such as Boost’s Shared Pointer. These type of pointers are sometimes referred to as “smart pointers”.
The way Boosts Shared Pointer works is that it is actually an object that wraps up the pointer to the memory and a pointer to its reference count. The shared pointer class implements a copy constructor and an assignment operator so that when shared pointers are copied / assigned to each other, they will copy the two pointers (the one to the memory and the one to the reference count), and then they will increment the reference count. When a shared pointer is destroyed, its destructor decrements the reference count and checks if it is 0. If it is, then the reference count and the object the shared pointer is pointing to are both deleted, thus freeing up the memory. See the animation below for a demonstration of this.
Objective-C (Reference counted)
Note that this section aims to give an overview of how manual reference counting works in Objective-C. If you’re reading this, you’re most likely working on iOS (or just curious :)), in which case you should really be using Automatic Reference Counting (ARC), which was introduced in iOS SDK 5.0 (Xcode 4.2), and is available when targetting iOS 4.0 and above. You should probably still read this to get an idea of how retain/release works, then read up on ARC, which handles all your retains/releases for you. There is a good blog post on it here.
When using reference counting in Objective-C (Objective-C supports both reference counting and garbage collection, but garbage collection is only available on OSX, not iOS), up until iOS SDK 5.0, you need to manually retain/release your objects. Every time you call [myObject retain], you’re adding 1 to the reference count. Calling [myObject release] will subtract 1 from the reference count. When the reference count is 0, the object is deallocated. Note that when you alloc/init, new or copy an object, it has a retain count of 1. See the memory management documentation on Apple’s website for more information.
Objective-C also has the idea of an autorelease pool, which allows objects to have release called on them at a later date. Objects can be added to the autorelease pool by calling [myObject autorelease]. Whenever the autorelease pool is drained, every object that has been autoreleased will have [myObject release] called on it. This is useful for allowing you to allocate and autorelease an object in one line, so you don’t have to worry about cleaning up. For example, in iOS, you might see something like the following:
// The label is allocated and added to the autorelease pool in one line // myLabel is created with a retain count of 1 UILabel* myLabel = [[[UILabel alloc] initWithFrame: labelFrame] autorelease]; // The label is added to the main view // Internally, the main view will retain myLabel, giving it a retain // count of 2 [self.view addSubview: myLabel]; // ... // Your function exits, and the next time the autorelease pool is drained, // myLabel will have release called on it, which will decrement the retain count // As myLabel is still retained by the main view, it will not be deallocated // until it is also released by the main view
Autorelease pools are quite useful, as you don’t need to make sure you call release when you’re done with an object, just make sure you always call autorelease on the same line you alloc/new/copy. Note that each thread has its own autorelease pool, and if you create your own threads, you need to manually create an autorelease pool for your thread. See the documentation for NSAutoreleasePool.
So that hopefully gives you a bit more insight into reference counted memory management. In the final part of these posts, we’ll look at garbage collection.