메모리 구조
Stack 영역
- 값 타입이 할당된다
- 함수 호출 시, 지역변수, 매개변수, 반환 값, 파라미터 값 등이 저장된다.
- 함수 호출이 완료되면 사라진다. -> 메모리를 직접 해제해주지 않아도 된다!
- 컴파일 시 크기가 결정된다.
- 메모리가 한정되어있다.
- 함수도 들어간다.
Heap 영역
- 대체로 참조 타입값(클래스 인스턴스, 클로저)이 할당되지만, 값 타입도 할당될 때가 많다.
- class안에 struct가 있으면 struct도 class라는 컨테이너 안에 넣어져서 같이 Heap에 할당된다.
- 유일하게 런타임 시 크기 결정된다.
- 사용하고 난 후에는 반드시 메모리 해제를 해줘야한다.(memory leak 위험성) -> Swift에서는 ARC가 직접 해준다.
- 프로그래머가 할당 및 해제를 해줘야된다.(동적 할당)
- 할당 작업, 해제 작업으로 인해서 속도 저하(Stack에 비해서)
- 메모리가 한정되어 있지 않다.
그러나! 값 타입이라고해서 항상 stack에 저장되는 것은 아닙니다!..
기본적으로 가변길이 Collection 타입(Array, Dictionary, Set)과 String은 데이터를 Heap 저장합니다. Collection 타입 다 struct으로 구현이 되어있지만 말입니다.
이렇게 구현된 이유는 컴파일 타임에 사이즈를 정확히 유추하기가 어려워서 Heap에 할당하여, 사이즈를 크게 작게 만드는 것입니다.
만약 아래와 같은 코드가 있다고 생각해봅시다!
// 힙에 저장이된다.
var str = "abcdefghijklmnopqrstuv"
// 이렇게 값이 변경되면, 스택 영역 주소 값은 그대로이지만, 힙 영역 주소 값은 변경이된다.
// 왜냐하면 값 타입이니까!
str = "Asdfsafasfsafasfasfvsevasefasf"
그리고 부가적으로 값 타입의 경우, COW 기법을 활용합니다. 값이 변경되기 전까지는 원래 값을 계속 참조하고 있는 것이죠! (Heap 저장되니까 가능하다!) 그래서 string도 복사를 해도, 해당 복사된 string이 변경되기 전까지는 서로 같은 힙 영역 주소 값을 가리키고 있습니다.
또한, reference type 안의 값 타입 프로퍼티 또한 힙 영역에 저장이됩니다!
데이터 영역
- 전역변수, static 변수가 저장된다.
- 프로그램 시작과 동시에 할당
- 프로그램 종료 직후 메모리 해제
- 실행 도중 값이 변경될 수 있으므로 Read-Write로 지정이된다.
- 메타 타입이 할당이된다.
코드 영역
- 소스 코드가 기계어 형태(0,1로만)로 저장된다.
- 컴파일 타입에 결정되며, Read-Only 형태
- 함수, for문 등이 저장된다.
- 함수의 실행은 Stack에 공간을 할당한다.
-> 아래의 링크 참고해보기!
https://shark-sea.kr/entry/iOS-Swift-메모리의-Stack과-Heap-영역-톺아보기
Automatic Reference Counting
- 참조 타입인 클래스 인스턴스에만 적용이된다. → 즉, 힙 영역을 관리한다.
- 즉, 컴파일 시 코드를 분석하여 자동으로 retain, release 코드를 생성해주는 것!
- 참조된 횟수를 추적해서 더 이상 참조되지 않은 인스턴스를 메모리에서 해제해주는 것이다.
예를 통해서 ARC가 필요한 상황을 이야기해보자, 아래와 같이 Human Class를 정의하고, miro라는 클래스 인스턴스를 생성했다고 하자. 그리고 miro는 지역변수라고 가정!
class Human {
var name: String?
var age: Int?
init(name: String?, age: Int?) {
self.name = name
}
}
let miro = Human(name: "Miro")
- 그러면 아래와 같이 miro는 지역변수 이므로 스택에 할당이되고, Human 인스턴스는 힙에 할당이된다.
- 그리고 아래와 같이 클론을 만들어서 주소값을 복사해보자.
- 그리고 만약 함수 종료 시점에 miro와 clone이 사라지게 된다면 아래와 같은 그림과 같이 될 것이다.
그렇다면 힙에 있는 인스턴스는 누가 메모리 해제를 해줄까? 바로 ARC가 해준다!
ARC in Action
어떠한 방식으로 ARC가 작동하는 알아보자
- 아래와 같이 Person Class를 정의해보자
class Person {
let name: String
init(name: String) {
self.name = name
print("\\(name) is being initialized")
}
deinit {
print("\\(name) is being deinitialized")
}
}
- 그리고 Person 클래스를 할당한 3개의 변수를 선언해보자. 세개의 변수 다 옵셔널로 지정하였기에 초기값은 전부 다 nil이다.
var reference1: Person?
var reference2: Person?
var reference3: Person?
- 아래와 같이 Person 인스턴스와 reference1은 강한 참조를 하게 되고, reference2와 reference3에도 할당이되면서 2개의 강한 참조가 추가로 카운팅이 됩니다. 따라서 최종적으로 reference counting은 3입니다.
reference1 = Person(name: "John Appleseed") // reference count : 1
// Prints "John Appleseed is being initialized"
reference2 = reference1 // reference count : 2
reference3 = reference1 // reference count : 3
- 그리고 이제 할당 해제를 해보겠습니다. reference1과 reference2에 nil을 할당하여 강한 참조 두개를 제거할 수 있습니다. 그러나 아직 한개의 강한 참조가 남아있기에 deinit이 실행되지는 않습니다.
reference1 = nil // reference count : 2
reference2 = nil // reference count : 1
- 최종적으로 할당 해제를 진행해주면 참조 카운트가 0이 되면서 메모리에서 해제가된다.
reference3 = nil // reference count : 0
// Prints "John Appleseed is being deinitialized"
이를 통해서 ARC는 인스턴스를 생성하면 참조 카운트를 추적하고 더 이상 필요하지 않을 때(참조 카운트 = 0)일 때에 인스턴스를 메모리에서 해제한다는 것을 알 수 있었습니다. → By ARC
Strong Reference Cycles Between Class Instances(강한 순한 참조)
강한참조(Storng Reference)
인스턴스의 주소값이 변수에 할당될 때, Reference Count가 증가하면 강한 참조이다.(Default로 강한 참조가 된다.)
강한 참조 시 참조를 한 인스턴스가 해제되었음에도 계속해서 참조를 유지하는 문제가 발생하는데, 아래의 코드를 통해서 해당 문제를 알아보자.
- 아래와 같은 class 두 개를 정의해주자.
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment? // 옵셔널로 초깃값은 nil
deinit { print("\\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person? // 옵셔널로 초깃값은 nil
deinit { print("Apartment \\(unit) is being deinitialized") }
}
- 아래와 같이 클래스 인스턴스를 만들어보자.
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed") // Person reference count : 1
unit4A = Apartment(unit: "4A") // Apartment reference count : 1
- 그러면 아래의 그림과 같은 강한 참조 그림을 그릴 수 있습니다.
- 그러면 각자의 프로퍼티에 클래스 인스턴스를 한번 할당 해보자. 그러면 Apartment의 RC는 2가 되고, Person의 RC는 2가됩니다.
john!.apartment = unit4A // Apartment reference count : 2
unit4A!.tenant = john // Person reference count : 2
- 그러면 아래와 같은 그림으로 강한 참조를 나타낼 수 있습니다.
- 그리고 난 후, 변수에 데이터를 할당 해제하게 되면, 강한 참조가 사라지게 되면서 참조 카운트는 1이 됩니다.
john = nil // Person reference count : 1
unit4A = nil // Apartment reference count : 1
→ 즉, Person instance와 Apartment 인스턴스 사이의 강한 참조가 유지되면서 불필요한 데이터가 메모리 상에서 해제되지 않고 유지가 되는 문제가 발생합니다. 즉 서로 instance끼리 참조를 하고 있어 RC가 0이 되지 않은 것이다!
‼️ Person 객체와 Apartment 객체가 몇번이나 할당이 되었는 지를 통하여 RC를 계산한다!
Resolving Strong Reference Cycles Between Class Instances
weak(약한 참조)
- 인스턴스를 참조할 시에 RC를 증가시키지 않는다.
- 인스턴스가 메모리에서 해제가 된 경우에 자동으로 nil을 할당하여 메모리가 해제가된다.
- 무조건 옵셔널 타입의 변수여야한다.
→ 아래와 같이 순한 참조를 일으키는 프로퍼티 앞에 weak를 붙혀주면 된다.
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment? // 옵셔널로 초깃값은 nil
deinit { print("\\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person? // 옵셔널로 초깃값은 nil
deinit { print("Apartment \\(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed") // Person reference count : 1
unit4A = Apartment(unit: "4A") // Apartment reference count : 1
- 그리고, 아래와 같이 할당을 해주면 weak의 경우 약한 참조를 하므로 RC가 증가하지 않는다.
john!.apartment = unit4A // Apartment reference count : 2
unit4A!.tenant = john // Person reference count : 1
- 따라서 만약 아래와 같이 nil을 할당할 경우 Person의 RC는 0이 되어 바로 메모리를 해제해버리고, Apartment의 경우 RC가 1이지만 참조하고 있는 Person 메모리 해제되므로 최종적으로 RC가 0이된다.
john = nil // Person reference count : 0, Apartment 4A is being deinitialized
unit4A = nil // Apartment reference count : 0, John Appleseed is being deinitialized
unowned(미소유 참조)
- 인스턴스를 참조할 시에 RC를 증가시키지 않는다.
- 인스턴스를 참조하는 도중에 해당 인스턴스가 메모리에서 사라질 일이 없다고 확신 → 참조하던 인스턴스가 메모리에서 해제된 경우, nil을 할당받지 못하고 해제된 메모리 주소값을 계속해서 들고 있는다.
→ unowned로 선언된 변수가 가르키던 인스턴스가 메모리에서 먼저 해제가 된 경우, 해당 변수에 접근을 하려고 하면 에러가 발생하게 된다.
- 그리고, weak는 런타임에 nil이 될 수 있기에 optional로 선언되어야하고, var로 선언되어야한다.
- 그러나, unowned의 경우 위에서 설명한 바와 같이 참조 도중에 해당 인스턴스가 메모리에서 사라질 일이 없다고 확신하기에 옵셔널로 선언하지 않는다!
- 아래와 같이 weak의 경우 인스턴스를 nil로 할당할 수 있다.
- 그러나, unowned의 경우 weak와는 다르게 unowned로 설정된 값을 nil로 설정하지 않는다!(계속 메모리 값을 가지고 있어서 접근하면 error가 난다!) 구체적으로 어떤 상황인 지는 맨 밑에서 알아보자!
다른 예를 통해서 알아보자.
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\\(number) is being deinitialized") }
}
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
- 아래와 같이 Customer instance의 RC는 1, CreditCard instance의 RC은 1이다.
- 그리고 아래와 같이 nil을 할당을 해주자.
john = nil
- 그러면 아래와 같이 Customer instance의 RC는 0이 되고(메모리에서 해제가 된다), CreditCard instance의 RC도 0이 된다.
- 만약 위의 코드 말고 weak와 동일한 코드에서 weak와 unowned를 가정하고 john에게 nil을 할당하고 unit4A.tenant에 접근한다고 가정해보자.
// **weak**
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment? // 옵셔널로 초깃값은 nil
deinit { print("\\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person? // 옵셔널로 초깃값은 nil
deinit { print("Apartment \\(unit) is being deinitialized") }
}
// **unowned**
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment? // 옵셔널로 초깃값은 nil
deinit { print("\\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
unowned let tenant: Person
deinit { print("Apartment \\(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
- 그리고 john에게 nil을 할당한다. 그리고 unit4A의 tenant에 접근해보면?
john = nil // Person 인스턴스의 reference count = 0, Apartment 인스턴스의 rc = 1
print(unit4A.tenant) // crash가 난다!.. weak였으면 nil이 나왔을텐데!
- weak일 경우 -> nil
- unowned일 경우 -> error
그렇다면? 항상 weak를 쓰는 게 좋지 않을까?
-> NO, weak는 객체를 계속 추적하고 있다. 객체를 계속 추적하는 것은 런타임시 오버헤드가 발생할 수 있다!
참고자료
https://babbab2.tistory.com/27https://zeddios.tistory.com/1213
'iOS' 카테고리의 다른 글
[iOS] MVC (0) | 2023.02.04 |
---|---|
[iOS] Singleton(싱글톤) (0) | 2023.02.04 |
[iOS] 의존성 주입 (0) | 2023.02.04 |
[iOS] Delegate Pattern (0) | 2023.02.04 |
[iOS] Queue 구현하기 (0) | 2023.02.04 |