'ARC'는 Automatic Reference Counting의 준말이다. 말그대로 자동으로 인스턴스가 현재 참조되고 있는 횟수(Reference Count)를 '숫자로' 카운팅하여 0이될때 힙(Heap) 메모리에서 해제해주는 메모리 관리 방식이다.
ARC는 이름에서도 알 수 있듯이 Reference 즉, 참조 타입인 class 인스턴스의 참조 횟수를 카운팅 해주고, 자동으로 메모리 해제를 해주고 있기 때문에 개발자가 힙 메모리 관리를 신경 쓸 필요가 없다.
RC(Reference Count)가 0이될 때 메모리에서 해제하는 이유는, 더 이상 사용하지 않는 메모리라 생각하여 해제해주는 것이다. 참고로 모든 인스턴스는 자신의 RC(Reference Count) 값을 가지고 있다.
힙 얘기가 나와서 메모리 구조에를 간략하게 설명하고 넘어가자면 아래와가 같이 4가지 종류가 있다.
1) 코드 - 소스코드가 기계어(0 또는 1)로 저장되는 메모리 영역이다.
2) 데이터 - 전역변수, static 변수가 저장되는 메모리 영역이다.
3) 힙 - 프로그래머가 동적할당하는 메모리, 클래스의 인스턴스, 클로저가 저장되는 메모리 영역이다.
4) 스택 - 함수 호출시 함수의 지역변수, 매개변수, 리턴 값이 저장되는 메모리 영역이다.
(이번 포스팅은 메모리 구조가 아닌 ARC가 주제이므로 메모리 구조에 대한 자세한 내용은 시리즈 CS에서 자세히 다루도록 하겠다.)
ARC는 메모리 구조 4계층 중 힙(Heap)을 관리해주는 녀석이다.
그리고 클래스의 인스턴스와 클로저는 참조 타입(Reference Type)은 자동으로 힙에 할당된다!
🙋 ARC의 동작방식을 이해하는 것이 꽤 중요하다.
왜냐하면 strong, weak, unowned 개념, 클로저(Closure) 개념과도 관련 있기 때문에, ARC의 동작방식을 이해해두면 뒤에 포스팅되는 클로저, 강한순환참조 개념을 이해할 때 도움이 될것이다.
(참고 : 개발자 소들이 https://babbab2.tistory.com/26)
ARC 개념을 이해하는데 개발자 소들님의 블로그가 도움이 많이 되었다.
감사합니다...🙏
소들님 블로그에 있는것을 내 방식으로 실습해보고 아래 2.1~2.2 목차에 정리해보았다.
말로 하는 것보다.. 모두 실습을 통해 확인해보자!
let justdotheg = Employee(name: "Just Do The G", dept: "marketing")
Employee라는 클래스를 선언해두었다.
그리고 justdotheg라는 인스턴스를 생성하고 값을 초기화했다.
이때 RC(참조 횟수)가 +1이 된다.
justdotheg라는 인스턴스는 지금 전역에 선언되어 Data 영역에 메모리가 잡혀이겠지만, 지역변수라 생각하고 Stack에 할당되었다고 생각하고 그림을 그려보았다.
👇 그림 참조
let james = justdotheg
이 코드를 선언하면 james는 justdotheg라는 인스턴스를 레퍼런스 참조하게 된다.
이때 Employee 클래스의 RC(참조 회수)가 +1이 된다.
메모리 상으로는 다음과 같이 된다.
👇 그림 참조
목차 2.1 에서 사용하던 예제를 조금 변형해보았다.
makeEmployee( )라는 펑션을 만들어 james라는 인스턴스를 만드는 코드를 추가한 것이다. 참고로 james라는 인스턴스는 makeEmployee( ){ } 코드 블럭이 해제되었을 떄 메모리에서 해제된다.
RC(Reference Count) 입장에서 보았을 때,
아래 1)번 코드가 실행되면 Employee Instance의 RC는 1이 된다.
let justdotheg = Employee(name: "Just Do The G", dept: "marketing") //1️⃣ Instance RC : 1
아래 2)번 코드가 실행되면 Employee Instance의 RC는 2이 된다.
func makeEmployee(_ origin: Employee) {
let james = origin //2️⃣ Instance RC : 2
}
아래 3)번 코드가 실행되면 2번 펑션 블럭이 끝나면서 Employee Instance의 RC는 -1이 되어 값이 1이 된다.
makeEmployee(justdotheg) //3️⃣ Instance RC : 1
아래 그림에서 코드 1), 2), 3)에 따른 메모리와 RC 상태 변화를 봐보자.
1) 코드가 진행될 때
2) 코드가 진행될 때
💁🏻♂️ 펑션이 블럭이 끝나면서 메모리가 인스턴스가 해제됨
3) 코드가 진행될 때
james = nil //1️⃣ RC - 1
justdotheg = nil //2️⃣ RC - 1
위 코드를 거치면 메모리상태는 아래와 같이 변한다.
1) james가 nil값이 될 때
james 인스턴스 nil로 초기화되면서 힙영역을 가리키고 있던 RC는 -1하여 1값이 된다.
2) justdotheg가 nil이 될 때
Employee Instance를 Reference하는게 0이 되면서 힙영역에서 Employee Instance는 해제된다.
var justdotheg: Employee? = Employee(name: "Just Do The G", dept: "marketing") //1️⃣
var james: Employee? = Employee(name: "James", dept: "marketing") //2️⃣
justdotheg = james //3️⃣
1번과 2번은 힙에 서로 다른 영역에 메모리가 생성되면서, 각각의 RC는 1이다.
⭐️ 3번이 실행되면서 justdotheg 인스턴스가 가리키던 힙의 메모리 주소는 james 인스턴스 메모리 주소로 바뀐다.
(결론) 그에 따라 justdotheg의 힙 RC는 +1이 되어 RC = 2, james 인스턴스의 힙 RC는 -1로 RC = 0이 된다. 결국 james 인스턴스의 RC는 0이 되었으므로 자동으로 힙 메모리에서 해제된다.
그림으로 봐보자!
1)과 2)이 실행되면 아래와 같다.
3)이 실행되면 아래처럼 바뀐다.
~3까지 보여줬던 예시와 하나 다른점을 추가해보았다.
Employee의 클래스의 프로퍼티 중에 Contact 클래스의 인스턴스인 contacts를 프로퍼티로 갖도록 추가해보았다.
그리고 justdotheg라는 Employee의 인스턴스를 생성하고, nil 값으로 초기해보았다.
결과는 어떨까?
justdotheg라는 Employee 인스턴스가 메모리에서 해제되면서 Employee 클래스의 소멸자가 호출된다. 그 후 justdotheg가 포함하고 있던 프로퍼티인 contacts 인스턴스도 메모리에서 해제되어 Contact 클래스의 소멸자가 호출되어 출력되는 것이다.
그림을 통해 실제 메모리와 RC가 변하는 과정을 봐보자!
1) justdotheg라는 인스턴스가 생성이 되면 아래와 같이 contact 인스턴스도 힙 메모리에 자동으로 생성되면서, 동시에 RC(Reference Count)가 +1된다.
2) 이 때 justdotheg 인스턴스에 nil 값으로 초기화 하면 다음 두가지 과정을 거친다.
𝟭 Employee 인스턴스의 RC가 0이 되면서 힙 메모리에서 해제된다.
𝟤 Employee 인스턴스를 참조하고 있던 Contact 인스턴스를 참조하고 있던 메모리가 해제되니 RC(Reference Count)를 -1하여 RC = 0이 된다.
그리고 힙 메모리에서 해제된다.
ARC와 GC의 가장 큰 차이점은 '참조를 카운팅 (Reference Counting)'하는 시점이다.
- ARC : 컴파일시
- GC : 련타임시 (프로그램 동작 중)
자세한 설명은 아래 목차에서 해보겠다.
- 장점 : GC와 달리 어플 실행 도중에 메모리 해제 시점을 실시간으로 추적하지 않으므로, RunTime 시점에 추가 리소스가 발생하지 않는다.
- 단점 : 강한 순환 참조는 ARC에서 관리해주지 않아 메모리 누수 가능성이 존재한다. (약한참조 weak, 비소유 참조unowned로 해결이 가능)
🙋 순환참조가 어떤것인지는 뒤에 포스팅에서 따로 다루어 보겠다.
- 장점 : RC에 비해 메모리 누수될 가능성이 낮음
- 단점 : GC의 장점이 되기도 하는 특징을 위해, 어플 실행 시점(Run Time)에 계속 추적을 하느라 추가 리소스가 필요하다. 따라서 성능저하가 발생할 수 있다.