꿈돌이랜드

코코아 인터널스 - 2장 메모리 관리 본문

Programming/iOS

코코아 인터널스 - 2장 메모리 관리

loinsir 2023. 8. 7. 20:41
반응형

2.1 메모리와 객체

운영체제가 관리하는 프로세스는 이론적으로 32bit인 경우 4GB까지의 크기를 가지는 가상 주소 공간에 접근할 수 있다. 64bit인 경우 18EB(2^64)까지 가능하다. macOS는 메인 메모리상의 사용하지 않는 공간을 페이지(가상 메모리 단위)로 나눠서 하드 디스크에 백업하는 기능을 제공한다. 반면 iOS는 하드 디스크가 없고 대신 플래시 메모리를 사용하기 때문에 늘 메모리가 부족하기 마련이다. iOS에서 읽고 쓰는 데이터는 프로그램을 실행하는 동안 사라지지 않지만, 사용하지 않는 읽기 전용 데이터는 페이지를 저장하고 메모리상에서 지워 버린다. iOS는 읽고 쓰는 데이터들의 총합이 일정 수준 이상 많아지면 메모리 부족 경고를 보내고, 그래도 부족하면 앱을 강제로 종료시켜서 메모리를 확보한다.

macOS와 iOS를 비롯한 운영체제는 프로세스 주소 공간보다 물리적인 메모리가 상대적으로 부족하기 때문에 가상 메모리 방식을 사용한다. 물리적인 메모리 보다 더 큰 가상 메모리를 다루기 위해서, CPU와 메모리 관리 유닛(MMU)에서 일정한 크기를 가진 페이지 단위로 나눠 메모리를 관리한다. 기본적으로 페이지 크기는 4KB 단위를 사용하며, 가상 메모리 페이지 중에 읽을 데이터가 없어서 페이지 실패(Page Fault)가 되면 디스크에서 4KB단위 씩 새 페이지를 읽는다. 이런 과정이 많아질수록 앱 성능에 좋지 않은 영향을 끼친다.

—> 운영체제 가상 메모리 파트의 연장

2.1.1 객체 인스턴스 생성

…중략

오브젝티브-C에서 객체 인스턴스를 만드는 경우 다음과 같이 두 단계로 작성한다.

Pen *aPen = [[Pen alloc] init];

Pne 클래스에서 +alloc 메시지를 보내면 힙 공간에 객체 인스턴스가 만들어진다. +alloc 메서드 구현 내용을 의사코드로 살펴보면 다음과 같다.

+ alloc {
	id newObject = malloc(self->clsSizeInstance, 0);
	newObject->isa = self;
	return newObject;
}

위처럼 클래스 객체에 +alloc 메시지를 모내면 내부에서는 malloc 계열 C함수를 호출한다. malloc 함수는 생성할 객체 메타 클래스에 명시된 속성 데이터 타입 크기를 확인해서 clsSizeInstance 크기만큼 힙 메모리를 할당한다. 이때 메모리를 할당하는 최소 단위는 16Byte다. 4Byte를 요청하면 16Byte를 할당하고, 24Byte를 요청하면 32Byte를 할당해준다. 64bit 커널을 기준으로 994Byte 이상 128KB이하는 512Byte 단위로 할당하고, 그 이상의 경우는 가상 메모리 페이지 단위인 4KB 단위로 할당한다.

오브젝티브-C 런타임 레퍼런스를 살펴보면, +alloc 메서드는 +allocWithZone: 을 호출하고 class_createInstance() 런타임 API를 호출하는데, 이 함수 내부에서 calloc() 함수를 호출한다. calloc()malloc()과는 달리 객체 크기만큼 메모리를 할당하고, 할당한 메모리 공간은 0으로 채워주기까지 한다. 덕분에 객체 속성들은 기본적으로 0으로 채워진다.

…중략

64bit 기준으로 메모리 할당 단위는 TINY, SMALL, LARGE로 구분한다. iOS는 iOS 7 부터 64bit 커널로 동작하고 있다.

…중략

단위 명칭메모리 할당 범위할당 단위
NANO1~255Byte16Byte
TINY256~992Byte16Byte
SMALL993~127KB512Byte
LARGE128KB이상4KB

64bit 커널 기준 메모리 할당 범위와 할당 단위

…중략

TINY 단위의 메모리 조각과 SMALL, LARGE 단위의 메모리 조각들이 구분없이 만들어지면, 작은 공간들이 많아도 큰 객체는 들어갈 수 없는 파편화가 빈번히 발생하게 된다. (→ 힙 파편화) 그래서 실제로는 아래 처럼 힙 메모리 내부에 공간을 MALLOC_NANO,`MALLOC_TINY`,`MALLOC_SMALL` , MALLOC_LARGE 영역으로 구분해서 할당한다.

메모리 영역과 가상메모리

OS X 10.6 부터 ‘magazine_malloc’ 이라는 개념이 도입됐다. 여기서 매거진 영역은 멀티스레드 환경에서 메모리 할당 영역에 대한 오버헤드를 줄이기 위해 스레드별로 메모리 관리하는 단위다. 지역(zone) 단위로 나눠서 처리하던 기존 메모리 관리 방식을 개선한 것이다. TINY 단위는 특이하게 스레드와 상관없이 최상위 수준에서 생성하고 각기 스레드에 할당하는 구조로 동작한다. 이렇게 동작하는 이유는 대부분 오브젝티브-C 객체 인스턴스를 TINY 단위 크기 보다 작은 크기로 생성한다고 가정했기 때문이다. 따라서 객체를 설계할 때 1KB 보다 작은 TINY영역이나 256Byte 이하 NANO 영역 내에 들어가도록 만드는 것이 적당하다. 1KB 보다 큰 객체의 경우는 다음 그림의 NSData 처럼 처리한다.

위 그림과 같이 힙 공간에는 객체 인스턴스만 존재하는 것이 아니라, 이미지 리소스가 많은 앱이라면(이번에 개발한 런웨이같은 앱) ImageIO 영역에 비트맵 이미지가 캐싱되기도 하고, 캐싱을 위해서 CALayer 데이터도 가상 메모리 상당 부분을 차지하게 된다. .

...중략

특히 UIImage 클래스의 +imageNamed: 메서드로 이미지를 생성할 경우, 개발자가 의도하지 않더라도 시스템 내부에 이미지를 캐싱하기 때문에 각별한 주의가 필요하다.

코코아 프레임워크는 +allocWithZone: 메서드나 +copyWithZone: 메서드가 여전히 남아있다. 객체가 다른 객체를 생성할 때 같은 지역의 메모리 또는 같은 가상 메모리 페이지에 할당하면, 지역성이 좋아져서 프로그램 성능이 향상되곤 했다. 오브젝티브-C 2.0 이후 최신 런타임 구조부터는 객체 속성이나 메서드 조차도 메모리 할당 단위의 같은 지역에 있다는 보장이 없으며, 앞서 설명한 VM 내부 영역 관리만으로도 충분해서 지역을 관리할 필요학 없어졌다. NSObject에 대한 애플 개발자 문서에도 +allocWithZone: 메서드 처럼 지역 관련 메서드들은 더이상 사용하지 않는다고 명시되어있다.

2.1.1 객체 인스턴스 소멸

…중략

스위프트 객체에도 오브젝티브 -C 객체와 동일하게 소멸을 도와주는 -deinit 메서드가 있다. 차이점이 있다면, 오브젝티브-C 객체는 수동으로 메모리 관리 방식으로 구현해서 소멸 시점을 명시할 수 있지만, 스위프트는 ARC 방식을 사용하기 때문에 소멸 시점을 지정할 수 없다는 것이다.

2.1.3 요약

…중략

그럼에도 객체 인스턴스를 메모리에 생성해서 소멸할 때까지의 과정을 메모리 관리 측면에서 이해하고 있으면 더 효율적인 프로그램을 작성할 수 있다는 사실에는 변함이 없다. 객체 인스턴스가 아니더라도 이미지나 데이터베이스 캐시를 위해 내부적으로 할당하는 메모리 공간에 대해 종합적으로 고려해야 한다.

2.2 참조 계산

…중략

특정 객체가 다른 객체를 참조하는 경우, 참조할 객체가 메모리에 존재하는지 아니면 사라졌는지 판단할 필요가 있다. 애플은 메모리에 존재하는 객체 인스턴스를 확인하기 위해 참조 계산(reference counting) 방식을 제공한다.

코코아 프레임워크가 제공하는 모든 객체는 ‘참조 카운터’ 공간이 있다. 참조 카운터는 해당 객체의 참조 횟수를 계산한 값을 기록하는 공간이다. 참조 카운터에 저장하는 참조 계산 규칙은 단순하다. 객체가 만들어질 때 참조 횟수는 초기 값 1로 설정되고, 그 객체를 참조하는 다른 객체가 있을 때마다 참조 횟수가 1씩 증가한다. 반대로 참조하던 객체가 더 이상 참조하지 않으면 1 감소한다. 그러면 어느 시점에서든지 특정 객체를 참조하는 개수를 확인할 수 있다. 참조하는 개수가 없다면 그 객체는 메모리에서 해제해도 안전하다고 판단할 수 있는 것이다.

가비지 컬렉션은 사라짐 OS X 10.8 이전까지는 자바처럼 가비지 컬렉션 방식도 지원했지만, 10.8부터는 공식적으로 지우너되지 않는다. … 중략 가비지 컬렉션은 iOS에서는 애초에 지원하지 않았다 … 중략

2.2.1 객체 소유권

다른 객체를 참조한다는 것을 C언어 스타일로 표현하면 다른 객체의 힙 메모리 주소를 포인터 변수에 담아 갖고 있는 것을 의미한다.

…중략

위험한 포인터(dangling pointer) 현상을 방지하기 위해서 객체 소유권을 명시적으로 관리할 필요가 있다. 객체 소유권은 객체 A가 객체 B를 참조하는 동안만큼은 객체 B가 메모리에서 사라지지 않는다는 것을 명시적으로 보장해주는 방법이다.

객체 소유권 규칙

코코아에서 사용하는 일반적인 객체 소유권 규칙은 다음과 같다.

  • 특정 객체를 새로 만드는 경우는 소유권을 갖는다.
  • 다른 객체가 생성한 객체를참조하기 전에 소유권을 요청해서 받아야 한다.
  • 소유권을 얻는 객체를 더 이상 참조하지 않으면 소유권을 반환한다.
  • 소유권을 갖고 있지 않는 객체를 반환하면 안 된다.

…중략

소유권을 갖는다는 표현은 앞서 설명한 참조 계산식에서 참조 횟수를 1 증가한다는 의미와 같다. 반대로 소유권을 반환한다.는 참조 횟수를 1만큼 감소한다는 의미다.

객체 메서드와 소유권 규칙

앞에서 설명한 객체 소유권 규칙을 코코아 프레임워크 메서드와 고나련해서 풀어서 설명하면 다음과 같다.

  1. alloc, new, copy, mutableCopy 계열 메서드로 특정 객체를 생성하거나 복사하는 경우 새로운 객체 인스턴스를 만든다. 그리고 참조횟수를 1로 설정하고 소유권을 갖는다.
  1. 다른 객체가 이미 만들어 놓은 객체 인스턴스를 참조하는 경우에는 retain 메서드를 사용해서 객체 소유권을 요청한다. 그리고 참조 횟수를 1 증가시키고 소유권을 갖는다.
  1. 1, 2에서 소유권을 얻은 객체를 더 이상 참조하지 않는 경우, release 또는 autorelease 메서드를 사용해서 객체 소유권을 반환한다. 이때 참조횟수를 1 감소시킨다.
  1. 소유권을 갖고 있지 않은 객체는 반환하면 안된다. 객체가 1, 2에서 설명한 메서드로 소유권을 요청한 적이 없거나 이미 소유권을 반환한 경우에는 release 또는 autorelease 메시지를 보내면 안된다.

… 중략

2.2.2 자동 반환 목록

특정 객체에게 release 메시지를 보내면 참조 횟수가 1 감소하고, 0이 되면 그 즉시 dealloc 메서드를 호출하고 메모리를 반환한다.

어떤 객체는 생성하고 소유권이 없는 상태에서 다른 객체가 사용할 때까지 일정 시간 동안 메모리를 반환하지 않고 남아있어야 할 경우가 있다. 이런 경우를 대비해 자동 반환 목록 동작에 대해 알아보자. 자동 반환 목록은 일정 시간 뒤에 반환할 객체 목록을 만들어서 관리해준다.

오브젝티브-C 객체 인스턴스는 힙 메모리에 만들어지지만, 함수 범위나 문법적으로 특정 범위가 정해진 변수들은 C 언어처럼 자동 변수(지역 변수랑 같은 말)로 스택에 생겼다가 사라진다. autorelease 메시지를 받은 객체도 범위가 정해진 자동 변수와 비슷하게 동작한다.

…중략

release 메시지를 보내서 소유권을 즉시 반환하는 대신 autorelease 메시지를 보내면 자동 반환 목록에 객체를 등록할 수 있다. 특정 객체를 자동 반환 목록에 등록하면 autoreleasePool 객체가 소유권을 넘겨받는다. …중략

다른 작업을 처리하고, autoreleasePool 객체가 drain 메서드를 처리하면서 자동 반환인 객체를 차례로 release를 시킨다.

간편환 메서드와 자동 반환 대상

코코아 프레임워크 객체 중에는 객체를 생성하면서 자동 반환 목록에 추가하는 객체가 존재한다. 객체 팩토리 메서드 중에서 객체를 생성하기에 편리하도록 준비된 특별한 메서드는 객체를 생성하고 초기화한 다음 자동 반환 목록(autorelease)에 등록까지 해준다. 간편한 메서드의 클래스 이름 형태는 코코아 클래스 이름에서 NS 접두어가 없고 소문자로 시장한다. 예를 들어 NSString 클래스는 +string- 형태로 시작하는 +stringWithFormat, +stringWithString: 같은 메서드가 바로 간편한 메서드다.

…중략

자동 반환 목록 사용 시 주의 사항

자동 반환 목록을 사용할 때 주의할 사항이 있다. AutoreleasePool 객체는 대부분 NSRunLoop 클래스와 함께 동작하는데, 코드 흐름상 반복해서 객체를 생성하는 경우에는 자동 반환 목록에 있는 객체를 반환하는 시점이 되기도 전에 목록에 너무 많이 쌓이는 현상이 생길 수 있다. 이런 경우 목록에 소유권이 있는 객체들이 일시적으로 너무 많아져서 메모리 반환이 되지 않고 계속해서 메모리 사용률이 높아진다. 이럴 경우 반복문 안쪽에 AutoreleasePool 객체를 명시적으로 추가해서 강제로 반환할 객체들을 처리하는 방식이 필요하다.

for (nLoop = 0; nLoop < MAX_LOOP; nLoop++) {
	NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
	pen *temp = [[pen alloc] init];
	[temp autorelease];
	[autoreleasePool drain];
}

2.2.4 순환 참조 문제

…중략

상호 참조가 있는 객체에 release 메시지를 보내서 객체 소유권을 반환하면 힙 메모리의 객체는 정상적으로 반환될까? 그렇지 않다.

… 중략

메모리 누수의 원인은 바로 상호 참조에 있다. 이런 상호 참조는 객체가 두 개일 경우에만 발생하는 것이 아니다. 객체가 하나일 경우에도, 자기 자신을 참조하면서 참조 횟수를 1 증가시키면 동일한 문제가 발생할 수 있다. 뿐만 아니라 3개 이상의 객체 사이에서도 객체 그래프를 그렸을 때, 순환 사이클이 생기면 동일한 문제가 생긴다. …중략 순환 고리를 끊기 위해 적어도 어느 하나는 retain을 하지 않는 단순(또는 약한) 참조로 변경해야만 한다.


Uploaded by N2T

반응형