메모리 관리

메모리 관리

 

객체의 일생

 

살아있는 벌이나 새와 마찬가지로 프로그램의 객체에도 일생이 있다. 객체는 alloc 이나 new로 태어나고, 메시지를 받고 일을 하면서 살아가면서 컴포지션이나 메소드의 인수들을 통해 친구를 만들고, 일생이 끝날 때 해제되어 결국 죽는다. 객체의 일생이 끝나면 그 객체가 사용하던 메모리는 말끔하게 재활용되어 다음에 태어날 객체를 위해서 사용된다.

 

 

참조횟수

 

객체가 끝날때를 어떻게 알 수 있을까?

참조횟수라는 방법을 사용한다. 모든 객체는 그 객체의 참조횟수라고 하는 정수 값을 가지고 있다. 어떤 코드에서 그 객체를 사용하고 있다면, 그 코드는 객체의 참조횟수를 증가시켜서 "이 객체가 사용되고 있다" 라고 알려준다. 그 코드가 객체 사용을 다 끝내면 참조횟수를 줄여서 객체가 더 이상 사용되지 않음을 알려준다. 참조횟수가 0이 되면 아무도 그 객체를 사용하지 않는다는 의미이므로 소멸되고, 객체가 사용했던 메모리를 시스템에 반환하게 된다. 

 

객체가 alloc 이나 new 또는 copy 메시지(메시지를 받는 객체의 복사본을 만드는 메소드)를 통해 만들어지면 객체의 참조 횟수는 1이 된다. 

참조횟수를 증가시키려면 retain 메시지를 해당객체에 보낸다. 참조횟수를 줄이기 위해서는 그 객체에 release 메시지를 보내며, release 메시지를 보내는 것을 릴리즈 한다 라고도 한다.

 

참조횟수가 0이 되어 객체가 소멸되려고 할 때, 오브젝티브 c는 자동으로 dealloc 메시지를 해당 객체에 보낸다. 기존 객체의 dealloc 메소드를 오버라이드 할 수도 있다. 이렇게 함으로써 할당되었던 리소스들을 해제한다. 아울러 객체를 소멸시키기 위해 개발자가 직접 dealloc을 호출하면 안된다! 참조횟수가 0이 돼서 객체가 소멸될 때는 오브젝티브  c 가 dealloc 메소드를 자동으로 호출해준다.

 

 

 

retain은 id를 반환한다. 그렇게 함으로써 참조 호출의 반환값으로 다른 메시지를 보낼 수 있다. 그리고 참조횟수를 증가시키고 나서 다른 작업을 바로 요청할 수 있다. 예를들어 [[car retain] setTire:tire atIndex : 2]; 는 car의 참조횟수를 증가시키고 setTire:atIndex 작업을 진행한다.

 

 

 

 

 

객체소유권

 

A라는 객체가 B라는 객체를 가리키는 인스턴스 변수를 가지고 있다면 A객체가 B객체를 소유하고 있다고 한다.

예를 들어, CarParts에서 자동차는 엔진과 타이어를 가리키는 포인터를 소유하고 있다.

객체를 만드는 함수 역시 마찬가지로 그 객체를 소유한다고 한다.

CarParts에서 main()은 새 자동차 객체를 만들기 때문에 main() 함수는 자동차를 소유하고 있다고 한다. 

 

 

RetainCount1 


#import <Foundation/Foundation.h>

@interface RetainTracker : NSObject
@end // RetainTracker

@implementation RetainTracker
- (id)init
{
	if(self = [super init])
    {
		NSLog (@"init: Retain count of %lu.", [self retainCount]);
	}
	
	return (self);
} // init


- (void) dealloc
{
	NSLog (@"dealloc called. Bye Bye.");
	[super dealloc];
	
} // dealloc

@end // RetainTracker

int main(int argc, const char * argv[])
{
    RetainTracker *tracker = [RetainTracker new];
    // count: 1
	
    [tracker retain]; // count: 2
    NSLog (@"%lu", [tracker retainCount]);
	
    [tracker retain]; // count: 3
    NSLog (@"%lu", [tracker retainCount]);
	
    [tracker release]; // count: 2
    NSLog (@"%lu", [tracker retainCount]);
	
    [tracker release]; // count: 1
    NSLog (@"%lu", [tracker retainCount]);
	
    [tracker retain]; // count 2
    NSLog (@"%lu", [tracker retainCount]);
	
    [tracker release]; // count 1
    NSLog (@"%lu", [tracker retainCount]);
	
    [tracker release]; // count: 0, dealloc it
	
    return (0);
}

 

 

문제는 하나 이상의 항목이 특정한 객체를 소유하고 있을 때 발생하는데, 참조횟수가 1을 넘는 때가 바로 이런 경우다. RetainCount1 프로그램을 보자. main 함수는 RetainTracker 객체를 소유하고 있으므로, main 함수에서 그 객체를 정리할 책임이 있다.

 

Car의 엔진 세터 메소드를 다시 살펴보자.

 

- (void) setEngine: (Engine *) newEngine;

 

- (void) setEngine: (Engine *) newEngine

{

    engine = newEngine;

 

 

그리고 main 에서 아래오 같이 호출된다.

 

Engine *engine = [[Engine alloc] init];

[car setEngine:engine];

 

이제 엔진은 누가 소유하고 있는 것일까? main()인가 Car 인가? 

Engine이 더 이상 사용되지 않을 때 누가 Engine에 릴리즈 메시지를 보내야 할까?

Car가 Engine을 사용하고 있기 때문에 main이 될 수 없다.

아울러 main 이 나중에 엔진을 사용할 수도 있기 때문에 Car도 될 수 없다.

 

한가지 요령은 Car가 엔진의 참조횟수를 2로 늘리도록 하는 것이다.

그렇게 되면 두 항목 Car와 main 모두 엔진을 사용할 수 있게 되므로, 논리적으로 말이된다.

 

Car는 엔진 안의 setEngine: 에서 참조 횟수를 늘려야 하고, main 에서는 엔진을 릴리즈해서 참조횟수를 줄인다.

엔진의 사용이 끝나면 Car가 dealloc 메소드에서 엔진을 릴리즈하고, 그러면 엔진의 리소스가 정상적으로 반환될 것이다.

 

 

접근자의 참조횟수 관리

 

첫번째 setEngine의 메모리 관리 버전은 다음과 같다.

 

- (void) setEngine: (Engine *) newEngine

{

    engine = [newEngine retain];

 

이것으로 충분하지 않다. main() 에서 다음과 같이 호출한다고 하자.

 

 

engine1에서 문제가 생겼다. 참조횟수가 여전히 1이다.

main()은 이미 engine1의 참조가 끝나서 릴리즈를 했는데, Car는 릴리즈하지 않았다. 

여전히 engine1의 참조횟수는 1이므로 engine1에 할당된 리소스가 해제되지 않았다. 

 

그렇다고 car 객체가 engine1을 사용하지도 않는다. 

우리는 이제 메모리 누수가 발생한 engine1을 가지고 있는 것이다. 

engine1 객체는 아무것도 하지 않고 메모리만 차지하고 있다. 

 

 

setEngine의 다음버전을 보자

 

 

이 코드는 engine1의 메모리 누수 문제를 해결한 버전이다.

그러나 newEngine과 이전 엔진이 같은 객체인 경우 문제가 발생한다. 

다음의 경우를 보자.

 

 

 

 

 

이 코드는 무엇이 문제일까?

[car1 engine] 은 참조횟수가 1인 engine을 가리키는 포인터를 반환한다.

setEngine은 [engine release]로 시작하는데, 이코드는 참조횟수를 0으로 만들어서 그 객체를 해제시켜 버린다.

이제 newEngine과 engine의 인스턴스 변수는 해제된 메모리를 가리키고 있는 것이다. 

 

다음은 setEngine의 개선된 버전이다.

 

 

 

 

우선 새 engine을 참조하고 newEngine이 engine과 같은 객체라면 참조횟수가 증가했다가 바로 줄어들것이다. 

그러나 참조횟수가 0으로는 가지 않으므로 인젠이 예상치못하게 없어지는 사태는 발생하지 않을 것이다. 

접근자 메소드에서 이전 객체를 릴리즈하기 전에 새 객체를 참조하면 안전하게 작업할 수 있게 된다.