ARC - 1. ARC란?

Choooose·2023년 5월 28일
0

Swift

목록 보기
5/5

ARC란 ?

인스턴스들의 참조 카운트 (Reference Count)를 계산해서 적절한 시점에 인스턴스를 자동으로 해제하도록 도와주는 도구

ARC의 역할

class의 인스턴스를 생성하게 되면 인스턴스는 Heap 메모리에 생성하게 된다.

인스턴스가 더 이상 사용하지 않게 되면 메모리에서 해제된다.
이때 인스턴스를 참조하고 있는 변수, 상수가 있다면 인스턴스는 이미 메모리에서 해제되었기 때문에 문제가 발생하게 된다.

ARC는 이러한 문제를 자동적으로 해결하기 위해 소스코드 사이에 retain, release 코드를 삽입하여 인스턴스가 생성, 참조하게될 시에 Reference Count를 증가, 인스턴스 참조하지 않을 시에 감소시켜 Reference Count가 0이되면 인스턴스를 메모리에서 해제하도록 하는 역할을 한다.

ARC의 실행 시기

ARC는 컴파일 시점에 실행된다.

컴파일 시점에 retainrelease 코드를 삽입하게 된다.

따라서 런타임에는 단순히 실행되면서 삽입된 retain, release 코드를 통해 Reference Count를 증가/감소 시키기 때문에 ARC로 인한 오버헤드는 발생하지 않는다.

또한 개발자 입장에서도 언제쯤 Reference Count의 증가와 감소가 일어날지 예상할 수 있다.

그러나 순환참조 등의 문제가 발생할 시에 컴파일 시점에 이미 retain, release 코드를 넣어두었고 런타임에는 관여하지 않기 때문에 메모리 누수가 발생할 수 있다.

GC vs ARC

JAVA의 가비지 컬렉터는 ARC와 같이 Heap 메모리를 정리해주기 위해 수행된다.

그러나 ARC와는 몇가지 다른점이 있다.

  1. 런타임 시점에 Heap 메모리의 사용공간이 부족해지면 발생한다.
  2. 따라서 개발자 입장에서는 언제 GC가 동작할지 알 수 없다.
  3. GC가 수행되는 동안 GC를 수행하는 스레드를 제외한 모든 스레드의 작업이 중단된다. (Stop The World**)**
  4. GC는 각각 객체들을 순환하면서 참조 관계를 알아내고 마킹한다(Mark 과정).
  5. 마킹되어있지 않은 객체들을 메모리에서 제거한다 (Sweep 과정).

ARC와 다른점을 정리하면

ARCGC
동작 시점컴파일 타임런타임
개발자가 수행을 예측할 수 있는지예측 가능예측 불가능
동작 방식retain, release 코드 삽입Mark, Sweep
런타임 시 오버헤드발생 X ( 컴파일 타임에 수행 )발생 O ( 다른 스레드 중단 )
순환참조 발생 여부발생 O발생 X (런타임 시 자주 동작하여 해제)

순환 참조

순환 참조란 A인스턴스와 B인스턴스가 서로를 참조하여 메모리에서 해제되지 못하는 상황이다.

순환 참조가 발생할 시에 메모리 누수가 발생할 위험이 있기 때문에 사전에 방지해야한다.

순환 참조 예시

class Human {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    var developer: Developer?
    
    func printSummary() {
        if let developer = developer {
            print("\(developer.type) 개발자 \(name)")
        }
    }
    
    deinit {
        print("Human 해제")
    }
}

class Developer {
    var type: String
    
    init(type: String) {
        self.type = type
    }
    
    var human: Human?
    
    deinit {
        print("Developer 해제")
    }
}

위와 같은 두개의 클래스가 있다고 하자

Human은 이름을 가지는 클래스, Developer는 어떤 개발자인지에 대한 클래스이다.

Human 에서 developer변수는 Developer 인스턴스를 참조하고 printSummary() 라는 함수에서는 Human의 이름이 developer의 어떤 개발자인지에 대해 출력하는 함수이다.

각 클래스의 deinit은 인스턴스가 메모리에서 해제되는 경우 호출된다.

두 클래스의 인스턴스를 만들어보자

var human: Human? = .init(name: "Taek")
var developer: Developer? = .init(type: "iOS")

인스턴스가 생성 되면 ARC는 retain 코드를 삽입하여 Reference Count를 1씩 증가시킨다.

인스턴스를 생성한 후 nil을 할당하여 메모리에서 해제해보았다.

human = nil
developer = nil

그 결과

위와 같이 정상적으로 해제되어 deinit이 호출되는 것을 확인할 수 있다.

이때 ARC는 release 코드를 삽입하여 Reference Count를 감소시킨다.

Reference Count가 0 이 되어 메모리에서 해제되고 deinit이 호출된 것이다.

이때 만약 Human 인스턴스와 Developer 인스턴스 각각의 developer, human 프로퍼티에 서로의 인스턴스를 할당해준다면?

var human: Human? = .init(name: "Taek")
var developer: Developer? = .init(type: "iOS")
human?.developer = developer
developer?.human = human
human?.printSummary()

위와 같이 printSummary() 함수에 대한 결과가 정상적으로 출력되는 것을 확인할 수 있고

서로 인스턴스 프로퍼티를 참조하기 때문에 Reference Count가 2로 증가한다.

여기서 각각의 인스턴스 변수에 nil을 할당하여 해제한다면?

실행 결과 deinit 호출되지 않았다.

human 변수와 developer 변수가 nil이 되면서 Reference Count는 감소하였지만
인스턴스가 서로를 참조하기 있기 때문에 제대로 해제되지 않은 것이다.

메모리 그래프로 확실하게 확인을 해보았다.

먼저 두 클래스 인스턴스를 생성만 해보았다.
그 결과 정상적으로 deinit이 호출되는 것을 확인하였다.

메모리 그래프에서도 정상적으로 해제되어 인스턴스가 존재하지 않는 것을 확인할 수 있다.

다음으로 클래스 인스턴스를 순환참조 시켜보았다.

순환참조가 되어 printSummary()가 실행된 것과 deinit이 호출되지 않은 것을 확인하였다.

메모리 그래프를 확인하였을때 순환참조가 발생하여 누수가 발생한다는 경고문과 순환참조가 되는 것을 확인할 수 있다.

따라서 순환 참조를 방지하기 위해 week, unowned 가 존재한다.

참조


자동 참조 카운트 (Automatic Reference Counting)

Documentation

ARC in Swift: Basics and beyond - WWDC21 - Videos - Apple Developer

[WWDC21] ARC in Swift: Basics and beyond

[Java] 가비지 컬렉션(GC, Garbage Collection) 총정리

How to detect iOS memory leaks and retain cycles using Xcode's memory graph debugger - DoorDash Engineering Blog

0개의 댓글