[iOS] GC와 ARC

Updated:

메모리 관리의 필요

모든 OS는 특정한 크기의 메모리를 가지고 있다. 이러한 한정된 공간의 메모리를 효율적으로 프로세스에게 할당해 주기 위해서는 메모리 관리가 필요하다. 오늘은 대표적인 메모리 관리 기법인 GC(Garbage Collection) 와 ARC(Automatic Reference Counting)에 대해 간략히 알아보고 두 기법의 차이점에 대해 알아 보도록 한다.

GC(Garbage Collection)

메모리에서 더 이상 사용되지 않는 인스턴스를 해제시켜서 메모리 관리를 해주는 기법을 말한다. 가비지 컬렉터라는 프로그램이 이 기법을 수행을 한다. 가비지 컬렉터는 mark-and-sweep 알고리즘을 통해서 메모리 해제 여부를 결정한다. GC 루트가 참조하는 모든 오브젝트 그리고 그 오브젝트들이 참조하는 다른 오브젝트들을 마킹 처리를 하고 마킹 처리가 되어있지 않은 오브젝트들은 메모리에서 해제시키는 sweep 과정을 거친다.

ARC(Automatic Reference Counting)

마찬가지로 애플리케이션의 메모리 사용량을 관찰하고 관리함과 동시에 인스턴스가 더이상 사용되지 않으면 그 메모리를 해제하는 역할을 한다. GC와는 전혀 다른 방법으로 메모리를 해제시킨다. 메모리(힙)에 할당된 참조 인스턴스들은 모두 RC(Reference Count)를 가지고 있는데, 다른 오브젝트들이 이 메모리의 인스턴스를 참조할 때마다 참조 횟수가 증가하게 된다. 메모리가 해제되는 시점은 바로 이 RC가 0이 되는 순간이다.

GC와 ARC 에서의 순환 참조(Retain Cycle)

ARC는 컴파일 시점에 메모리 관리 코드를 적절한 위치에 자동으로 삽입한다. 컴파일 시점에 언제 참조되고 해제되는지 결정되기 때문에 개발자가 참조시점을 파악을 할 수가 있으며 런타임 시점에는 추가적인 오버헤드가 발생하지 않는다. 하지만 순환참조로 인해 메모리 누수가 발생할 수 있다.

반면에 GC는 런타임 시점에 주기적으로 메모리의 참조를 추적하여 메모리를 관리하기 때문에 이로인한 오버헤드로 성능저하가 있으며 참조 해제 시점을 파악할 수가 없다. 다만 ARC와는 다르게 순환참조로 인한 메모리 누수가 발생하지는 않아 메모리 해제 가능성이 높다.

아래의 예시를 통해 순환참조 상황에서 최초의 오브젝트에 nil값을 넣엇을때 두 메모리 관리기법에서는 어떤일이 벌어지는지 보도록하자.
john과 ann은 서로 눈이 맞아서 사귀게 되었다. ??


class Man {
    let name: String
    var girlFriend: Woman?
    
    init(name: String) {
        self.name = name
    }
}

class Woman {
    let name: String
    var boyFriend: Man?
    
    init(name: String) {
        self.name = name
    }
}

var john: Man? = Man(name: "john")
var ann: Woman? = Woman(name: "ann")

// 순환참조 발생
john?.girlFriend = ann 
ann?.boyFriend = john

GC

위의 코드를 실행시에 GC로 메모리를 관리하게 되면 아래와 같은 그림이 된다. GC Root부터 시작해서 이 GC Root는 메모리의 힙 영역에 있는 Man 인스턴스와 Woman 인스턴스를 참조하게 된다. 참고로 john과 ann은 메모리의 스택영역에 존재한다.

GC_1



평생 모태솔로 살아온 ‘모솔로’라는 신이 이 커플의 애정행각을 눈뜨고 봐줄수 없어 벼락을 내리쳐 순식간에 없애버렸다. 두 존재가 없어졌기 때문에 nil값을 대입한다.

// 벼락 맞아서 사망
john = nil
ann = nil

GC_2

두 사람이 죽었기 때문에 john은 여자친구인 ann을, ann은 남자친구인 john을 서로 잃게 되었으니 그러한 존재자체도 없어져야 한다. 다시 GC 루트에서 시작해 Mark 처리를 진행하려고 하는데 애초에 도달가능한 인스턴스가 존재하지 않는다. 그러므로 도달 가능하지 않은, Mark 처리되지 않은 저 두 인스턴스를 메모리에서 해제시킨다.

GC_3

ARC

이제 ARC를 살펴보자. ARC는 GC와는 다르게 인스턴스가 참조될때 마다 RC(Reference Count)가 증가한다고 했다. john과 ann이 Man 인스턴스와 Woman 인스턴스를 참조했을때 +1, 그리고 Man 인스턴스의 girlFirend 프로퍼티가 Woman 인스턴스를 참조, Woman 인스턴스의 boyFriend 프로퍼티가 Man 프로퍼티를 서로 참조하여 +1이 되어, 두 인스턴스 모두 RC가 2가 되었다.

ARC_1

이제 변수 john과 ann에 nil값을 대입해보자. 참고로 RC가 감소하는 여러 조건이 있는데, 함수가 종료되어 지역변수가 스택에서 해제되었을 때나, 다른 참조체를 대입했을 때 등이 있다. 그리고 이 예시에서는 nil값을 대입 함으로서 RC를 -1 감소시킨다.

// 벼락 맞아서 사망
john = nil
ann = nil

ARC_2

음 그 다음에는? 예상했다시피 두 인스턴스는 메모리에서 해제되지 않는다. 전에 언급했듯이 ARC에서는 인스턴스의 참조횟수가 0이 되어야 힙 메모리에서 해제가 되게 때문이다. 서로가 서로를 참조함으로 인해서 순환참조가 발생했고 john과 ann의 존재는 사라졌지만, girlFriend ann과 boyFriend john이 Man과 Woman을 참조해 참조 카운트가 남게 되어 이로 인해 메모리 누수가 발생해 버렸다.

ARC_3

이에 대한 해결법은 어떻게 하면 될까? 간단하다. ‘약한 참조’를 하면 된다. 기본적으로 아무 선언도 하지 않으면 인스턴스를 참조할때 strong 방식으로 참조를 한다. 이를 강한 참조라고 하며 인스턴스의 참조횟수(RC)를 증가시킨다. 반면 weak로 선언을 하면 약한 참조를 한다고 하며 참조 횟수를 증가시키지 않는다. 그리고 참조하는 인스턴스가 메모리에서 해제되면(참조횟수가 0이 되면) nil값을 가지기 때문에 옵셔널 타입으로 선언된다.

그 외에 unowned 방식으로 참조하는 방법도 있는데, weak 처럼 참조 횟수를 증가시키지는 않지만 참조하는 인스턴스가 메모리에서 해제가 되면 nil값을 가지지 않아서 이미 메모리에서 해제된 인스턴스를 참조하는 위험상황이 발생할 수 있어 잘 사용되지는 않는다.


class Man {
    let name: String
    weak var girlFriend: Woman? // 약한 참조! RC를 증가시키지 않는다!
    
    init(name: String) {
        self.name = name
    }
}

class Woman {
    let name: String
    weak var boyFriend: Man? // 약한 참조! RC를 증가시키지 않는다!
    
    init(name: String) {
        self.name = name
    }
}

// 순환참조 발생안함! 약한 참조로 인해 참조횟수가 증가하지 않았기 때문!
john?.girlFriend = ann 
ann?.boyFriend = john

john = nil // 바로 해제됨
ann = nil // 바로 해제됨

Categories: ,

Updated:

Leave a comment