인스턴스들의 참조 카운트 (Reference Count)
를 계산해서 적절한 시점에 인스턴스를 자동으로 해제하도록 도와주는 도구
class
의 인스턴스를 생성하게 되면 인스턴스는 Heap
메모리에 생성하게 된다.
인스턴스가 더 이상 사용하지 않게 되면 메모리에서 해제된다.
이때 인스턴스를 참조하고 있는 변수, 상수가 있다면 인스턴스는 이미 메모리에서 해제되었기 때문에 문제가 발생하게 된다.
ARC는 이러한 문제를 자동적으로 해결하기 위해 소스코드 사이에 retain
, release
코드를 삽입하여 인스턴스가 생성, 참조하게될 시에 Reference Count
를 증가, 인스턴스 참조하지 않을 시에 감소시켜 Reference Count
가 0이되면 인스턴스를 메모리에서 해제하도록 하는 역할을 한다.
ARC는 컴파일 시점에 실행된다.
컴파일 시점에 retain
과 release
코드를 삽입하게 된다.
따라서 런타임에는 단순히 실행되면서 삽입된 retain, release 코드를 통해 Reference Count
를 증가/감소 시키기 때문에 ARC로 인한 오버헤드는 발생하지 않는다.
또한 개발자 입장에서도 언제쯤 Reference Count
의 증가와 감소가 일어날지 예상할 수 있다.
그러나 순환참조 등의 문제가 발생할 시에 컴파일 시점에 이미 retain
, release
코드를 넣어두었고 런타임에는 관여하지 않기 때문에 메모리 누수가 발생할 수 있다.
JAVA의 가비지 컬렉터는 ARC와 같이 Heap 메모리를 정리해주기 위해 수행된다.
그러나 ARC와는 몇가지 다른점이 있다.
Stop The World
**)**ARC와 다른점을 정리하면
ARC | GC | |
---|---|---|
동작 시점 | 컴파일 타임 | 런타임 |
개발자가 수행을 예측할 수 있는지 | 예측 가능 | 예측 불가능 |
동작 방식 | 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)
ARC in Swift: Basics and beyond - WWDC21 - Videos - Apple Developer
[WWDC21] ARC in Swift: Basics and beyond