My original project was leaking so I searched for the leak. When I found it I created a simple new project. The project uses ARC and the only code I added is the following.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
int elements = 10000000;
//memory usage 5,2 MB
NSMutableArray *array = [NSMutableArray arrayWithCapacity:elements];
//memory usage 81,7 MB
for (int i = 0; i < elements; i++) {
[array addObject:[NSObject new]];
}
//memory usage 234,3 MB
[array removeAllObjects];
//memory usage 234,3 MB
array = nil;
//memory usage 159,5 MB
}
After calling [array removeAllObjects] all NSObjects in the array should be deallocated and the memory usage should be 81,7 MB again. What am I doing wrong?
Here
NSMutableArray *array = [NSMutableArray arrayWithCapacity:elements];
you are creating autoreleased object (autorelease pool).
Many programs create temporary objects that are autoreleased. These objects add to the program’s memory footprint until the end of the block. In many situations, allowing temporary objects to accumulate until the end of the current event-loop iteration does not result in excessive overhead; in some situations, however, you may create a large number of temporary objects that add substantially to memory footprint and that you want to dispose of more quickly. In these latter cases, you can create your own autorelease pool block. At the end of the block, the temporary objects are released, which typically results in their deallocation thereby reducing the program’s memory footprint
Wrap with @autoreleasepool {}
method [NSMutableArray arrayWithCapacity:elements]
:
NSMutableArray *array;
@autoreleasepool {
array = [NSMutableArray arrayWithCapacity:elements];
// [NSMutableArray arrayWithCapacity:] creates object with retainCount == 1
// and pushes it to autorelease pool
// array = some_object; usually (and in this case also) is transformed by ARC to
// [array release]; [some_object retain]; array = some_object;
// so here array will have retainCount == 2 and 1 reference in autorelease pool
} // here autorelease pool will call `release` for its objects.
// here array will have retainCount == 1
or change it to
NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:elements];
You've been bitten by the dreaded autorelease pool. Essentially to make MRC (manual reference counting) manageable by people instead of releasing an object immediately it can be handed to the autorelease pool (an instance of NSAutoreleasePool
, it's documentation gives more details) which will retain the object until the pool is later drained. ARC (automatic reference counting) could be designed so the autorelease machinery is not needed, but to maintain compatibility with MRC is remains.
The pool automatically drained at the end of a run loop cycle - i.e. when the application has finished processing an event. However if an application creates a lot of temporary objects and then discards them is some localised part of the program then a using a local autorelease pool can drastically reduce the maximum memory use. It is not that such temporary objects will not be release, just that they will live far longer than needed. A local pool can be create with the @autoreleasepool { ... }
construct.
You can see the effect in your example by wrapping the whole of the body of applicationDidFinishLaunching:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
@autoreleasepool
{
...
}
}
and stepping through with the debugger.
In your real code you need to work back from the point which is producing lots of temporary objects to locate a suitable point to add an autorelease pool.
HTH.
Addendum
It is not the objects in your array that are not getting released when you think they should , you can test this by using a simple class which counts initialisations and deallocations, e.g.:
@interface TestObject : NSObject
+ (void) showCounts;
@end
@implementation TestObject
static uint64_t initCount = 0, deallocCount = 0;
- (id) init
{
self = [super init];
if(self) initCount++;
return self;
}
- (void) dealloc
{
deallocCount++;
}
+ (void) showCounts
{
NSLog(@"init: %llu | dealloc: %llu", initCount, deallocCount);
initCount = deallocCount = 0;
}
@end
Use this instead of NSObject
and call showCounts
after you are done with your test - try with/without autorelease, etc.
Your memory is always getting released, it is just the time at which it is release that is the issue. Some objects end up in an autorelease pool, either the default one which is emptied once per event, or a local one.
Unless you create a lot of temporary objects in response to a single event you normally won't see an issue. Consider whether you are chasing a real issue for your application here. If you are among the things to try to alleviate the problem are:
Avoid uses of convenience constructors of the form <name>WithX...
which are shorthands for [[[C alloc] initWithX...] autorelease]
. On many, but not all, occasions the compiler can remove such objects from the autorelease pool just after the convenience constructor returns (and your case appears to be one in which is can fail). The better route is to use alloc
/init
, new
(shorthand for alloc
/init
) or, if provided, newWithX...
(shorthand for alloc
/initWithX...
). Try these options with your example and see the differences in when (not if) the memory is released.
Well placed @autoreleasepool
blocks.
HTH
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With