Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS에서 자동 참조 카운팅(ARC)과 가비지 컬렉션(Garbage Collection)의 차이점에 대해 설명해주세요. #6

Open
Phangg opened this issue Mar 28, 2024 · 5 comments
Assignees

Comments

@Phangg
Copy link
Member

Phangg commented Mar 28, 2024

iOS에서 자동 참조 카운팅(ARC)과 가비지 컬렉션(Garbage Collection)의 차이점에 대해 설명해주세요.

  • 가비지 컬렉션의 동작 원리와 장단점에 대해 설명해주세요.
  • iOS에서 가비지 컬렉션을 사용하지 않는 이유와 ARC를 선택한 배경에 대해 설명해주세요.
@Phangg Phangg self-assigned this Mar 28, 2024
@Phangg
Copy link
Member Author

Phangg commented Apr 3, 2024

ARC - Auto Reference Counting

Swift 에서 메모리 관리를 자동으로 해주는 것으로 참조 카운트 (RC) 를 관리해주는 기술
참조 값이 사용되지 않을 때, RC를 자동으로 감소시켜주고 0이 되면 자동으로 매모리 해제 시킴
retain cycle ( 순환 참조 ) 을 주의해야 함
ARC 동작은 run time 이 아니라, compile time 에 실행 돰
과거 Objective-C 에서는 MRC 라고.. RC 를 수동으로 관리해주었음

MRC 와 ARC

스크린샷 2024-04-03 오전 10 52 39

순환참조 - retain cycle

메모리 누수 ( Memory Leak ) 가 발생 -> Weak, Unowned 을 사용하여 해결
동적으로 관리해준다고 했는데 run time 이 아닌, compile time 에서 어떻게 다 이루어지지?
compile 시점에서 코드를 분석하여, retain 과 release 를 코드 내부에 적절한 위치에 삽입
이후 run time 에 해당 retain, release 가 실행되면서 RC 를 관리해줄 수 있음

retain : 참조 카운트를 증가
release : 참조 카운트를 감소

Retain Cycle ?

메모리가 해제되지 않고 메모리 누수 ( Memory Leak ) 가 생기는 현상.
참조가 순환되고 있는 상태.

class Person {
    let name: String
    var home: Home?
    
    init(name: String) {
        self.name = name
        print("\(self.name) - Init")
    }
    deinit {
        print("\(name) - Deinit")
    }
}

class Home {
    let homeType: String
    var tenant: Person?
    
    init(homeType: String) {
        self.homeType = homeType
        print("\(self.homeType) - Init")
    }
    deinit {
        print("\(homeType) - Deinit")
    }
}

// Person retain
var lee: Person? = Person(name: "Lee")      // Lee - Init & rc = 1
// Home retain
var home: Home? = Home(homeType: "APT")     // APT - Init & rc = 1

// Home retain
lee?.home = home        // rc = 2
// Person retain
home?.tenant = lee      // rc = 2

// Person release
lee = nil               // rc = 1
// Home release
home = nil              // rc = 1

// 두 인스턴스가 서로 강한 참조를 통해, 순환 참조가 이루어짐
// rc 가 0 이 될 수 없는 상태, memory leak

해결을 위해,
Weak Reference 사용 ( default : Strong )
Unowned Reference 사용

Weak Reference : 약한 참조

class Person {
    let name: String
    var home: Home?
    
    init(name: String) {
        self.name = name
        print("\(self.name) - Init")
    }
    deinit {
        print("\(name) - Deinit")
    }
}

class Home {
    let homeType: String
    weak var tenant: Person?
    
    init(homeType: String) {
        self.homeType = homeType
        print("\(self.homeType) - Init")
    }
    deinit {
        print("\(homeType) - Deinit")
    }
}

// Person retain
var lee: Person? = Person(name: "Lee")      // Lee - Init & rc = 1
// Home retain
var home: Home? = Home(homeType: "APT")     // APT - Init & rc = 1

// retain X (weak)
lee?.home = home        // rc = 1
home?.tenant = lee      // rc = 1

// Person release
lee = nil               // Lee - Deinit & rc = 0
// Home release
home = nil              // APT - Deinit & rc = 0

weak 키워드는 한 쪽에만 붙여줘도 된다.
이때, 해당 프로퍼티의 타입이 항상 Optional 이어야 한다. ( 당연히, var )
-> 참조하는 인스턴스가 메모리에서 해제 되었을 경우, 자동으로 nil 을 할당하기 때문

Unowned Reference : 미소유 참조

class Owner {
    let name: String
    var company: Company?
    
    init(name: String) {
        self.name = name
        print("\(self.name) - Init")
    }
    deinit {
        print("\(name) - Deinit")
    }
}

class Company {
    let name: String
    unowned let owner: Owner
    
    init(name: String, owner: Owner) {
        self.name = name
        self.owner = owner
        print("\(self.name) - Init")
    }
    deinit {
        print("\(name) - Deinit")
    }
}

var jun: Owner? = Owner(name: "jun")                // jun - Init
jun?.company = Company(name: "xxx", owner: jun!)    // xxx - Init 

jun = nil                                           // jun - Deinit & xxx - Deinit

jun 이 nil 이 될 때, 참조하고 있던 company 도 같이 사라짐

참조 카운트가 0이 되지 않았는데 원본이 사라지면 Crash 에러가 난다.
원본의 주소를 그대로 가리키고 있기 때문.
따라서, life cycle 을 고려해야 한다.

약한 참조는 참조된 인스턴스의 수명이 더 짧을 때 항상 사용되고, 소유되지 않은 참조는 참조된 인스턴스의 수명이 같거나 길 때 사용된다.


GC - Garbage Collection

메모리 관리를 프로그램 실행중에 동적으로 관리해주는 기술
감시하고 있다가, 사용을 하지 않는 상황에 메모리를 삭제 해주는 것
( 어떤 변수도 가르키지 않게 된 메모리 )
run time 에 메모리가 관리 됨
( 메모리사용, CPU 점유가 생길 수 밖에 없음 )
ARC 에 비해, 인스턴스가 해제 될 가능성이 더 높음
( 메모리를 지속적으로 감시하고 있기 때문 )
어떤 메모리를 해제해야 할 지 결정할 때 사용되는 비용이 많이 들게 됨
( GC 의 알고리즘을 통해, 메모리 해제 시점을 찾아야하기 때문 )
또한, GC 가 실행되는 시간이나 타이밍을 제대로 알기 어려움
( 할당 해제 타이밍을 알 수 없음 )


iOS에서 가비지 컬렉션을 사용하지 않는 이유와 ARC를 선택한 배경..?

GC 의 경우, 런타임에서 작동하게 되는데
이때, 메모리와 CPU 를 사용하게 됨
기기의 메모리와 CPU 가 제한적인 모바일 기기에서 사용하기에 성능이 더 좋은 ARC 를 사용하지 않을까 생각 함

@kmh5038
Copy link
Member

kmh5038 commented Apr 3, 2024

순환참조가 아닐경우에는 강한 참조여도 괜찮은가요?

@Hminchae
Copy link
Member

Hminchae commented Apr 3, 2024

개발자의 실수에 의해 순환참조가 발생할 수 있는데, 이를 예방하기 위한 작업들에는 무엇이 있을까요?

@Phangg
Copy link
Member Author

Phangg commented Apr 3, 2024

순환참조가 아닐경우에는 강한 참조여도 괜찮은가요?

네! default 가 강한참조인데, 사이클이 생기지 않는다면 ARC 에서 자동으로 관리되기 때문에 그냥 두어도 괜찮습니다.

@Phangg
Copy link
Member Author

Phangg commented Apr 3, 2024

개발자의 실수에 의해 순환참조가 발생할 수 있는데, 이를 예방하기 위한 작업들에는 무엇이 있을까요?

제가 알기로는 실수를 할 수 있다는게 ARC 의 단점입니다..
그래서 개발자는 직접 weak , unowned 를 잘써서 메모리를 관리해야합니다.

동호님의 이슈를 보며 알게 된 부분이지만,
Autorelease Pool 을 이용하는 것도 하나의 방법일 수 있다고 생각합니다.

개발자의 실수? 라기 보다는 메모리가 해제되지 않을 수 있는 많은 데이터를 요구하는 로직에서 사용됨이 더 맞을 것 같아요!
사실 상, Objective-C 에서 사용되던 레거시 코드에 가깝다고 하네요. 현재 Swfit 의 언어 특성상, 구조체를 많이 사용하고 ARC 를 활용하기에 직접 사용할 일이 많지 않지만 알아두는게 맞는 것 같습니다 ㅎㅎ

아래는 Autorelease Pool 에 대한 글을 모아봤으니 읽어봐도 좋을 것 같아요!

Autorelease Pool 란? - 간단한 설명
Autorelease Pool 성능 테스트
Autorelease Pool 의 역사와 성능 테스트 등 - 영문
Autorelease Pool 의 역사와 성능 테스트 등 - 위 블로그 해석 및 요약 블로그

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants