iOS

iOS :: Swift 메모리의 Stack과 Heap 영역 톺아보기

상어(shark) 2022. 1. 25. 02:00

안녕하세요! 

상어입니다.

 

최근에 공부를 하면서 이 부분에 대해서는 꼭 블로그에 써야겠다라는 생각이 들었는데

그건 바로 Stack과 Heap입니다!

아마 많은 분들이 해당 부분에 대해서는 학교에서나, 개인적으로나 공부를 꽤 많이 하셨을거라 생각을 하는데,

그래서 저는 이론적인 부분에 대해 작성하는 것보다

실제로 Swift 메모리에서 Stack과 Heap이 어떻게 적재되는지에 대해 같이 살펴보고자 합니다ㅎㅎ

 

(사진으로 설명을 하다보니 스크롤이 많이 길 예정이에요😁)

 

 

Stack과 Heap

 

본격적으로 시작하기 이전에 그래도 Stack과 Heap이 뭔지에 대해서는 대략적으로 살펴보는게 좋겠쬬?!

간단하게 정리를 하자면

 

Stack

- 지역변수와 매개변수 등이 저장되는 영역

- 이 영역에 할당된 변수는 함수 호출이 완료되면 사라짐

- 컴파일 시 크기 결정

- ValueType이 할당됨

 

Heap

- 동적 메모리 할당을 위한 영역

- 프로그래머가 할당 및 해제를 해줘야 함

- 런타임 시 크기 결정

 

여기서 잠깐!

'Heap은 ReferenceType이 할당된다.'가 빠져있지요?

이유는 'Heap은 ReferenceType이 할당된다.'라는 말은 잘못된 정보입니다. 

 

엥,, 이게 무슨 소리야???

라고 생각하시는 분들도 계실거에요.

정확하게 말씀드리자면,

상황에 따라 Heap에는 ReferencyType, ValueType이 전부 할당될 수 있습니다.

 

자,, 그럼 여기에 대한 궁금증을 빨리 해결하기 위해 메모리 부분을 얼릉얼릉 볼까요?!

 

struct User {
    let name: String
    let age: Int
    let company: String
}

class Test {
    let testValue = 10
    let text = "반갑습니다."
    
    init() {
        hello()
    }
    
    func hello(){
        let hello = "안녕하세요."
    }
}

class MemoryExam {
    let nickname = "shark"
    let count = 10
    let cheolSuUser = User(name: "철수", age: 15, company: "구글")
    let test = Test()
    
    init() {
        run()
    }
    
    func run() {
        let youngHeeUser = User(name: "영희", age: 30, company: "애플")
        let copyCheolSuUser = cheolSuUser
        let copyTest = test
    }
}

func main() {
    let memoryExam = MemoryExam()
}

main()

 

위의 코드는 예제코드입니다. 이 코드를 통해 메모리에 어떻게 적재되는지 살펴보려 합니다~

맨 처음에 빈 Stack과 Heap이 있습니다. 

그리고, 처음으로 main()이 실행됩니다.

func main() {
    let memoryExam = MemoryExam()
}

main()

그러면 Stack에 실행된 main() 함수와 그 내부에 있는 지역변수인 memoryExam이 할당됩니다.

그 후, memoryExam 변수에 의해 MemoryExam Class가 호출됩니다.

 

class MemoryExam {
    let nickname = "shark"
    let count = 10
    let cheolSuUser = User(name: "철수", age: 15, company: "구글")
    let test = Test()
    
    init() {
        run()
    }
    
    func run() {
        let youngHeeUser = User(name: "영희", age: 30, company: "애플")
        let copyCheolSuUser = cheolSuUser
        let copyTest = test
    }
}

MemoryExam Class가 호출되면 해당 Class크기 만큼 영역이 잡히는데, 변수 nickname으로 인한 1개의 영역, 변수 count로 인한 1개의 영역, 변수 cheolSuUser로 인한 3개의 영역, 변수 test로 인한 1개의 영역이 필요하므로 총 6개의 영역만큼 할당됩니다.

여기서 왜 cheolSuUser는 영역이 3개 일까요? 

이유는, cheolSuUser는 Struct User를 담는 변수이고, Struct User는 내부적으로 name, age, company 변수를 가지고 있기 때문에 3개의 영역이 할당되는 것입니다.

동시에 Struct 자체는 Value Type임에도 Heap에 할당된 것이 보이지요?

이것 때문에 Heap에는 ReferenceType만 할당된다는 말은 잘못된 것이라고 볼 수 있습니다.

그래서 메모리 상으로는

이렇게 표시가 됩니다. 

MemoryExam Class 영역이 할당될 때는 공간만 잡아주기 때문에 처음부터 값이 할당되는 것이 아닌,

MemoryExam Class의 코드 한줄 한줄이 실행되면서 값이 할당되는 것이므로

해당 영역이 잡힘 -> 값 할당 순서로 하면 됩니다.

 

그럼 값을 하나씩 할당해볼까요?

nickname에는 "shark"

count에는 10

cheolSuUser에는 User의 name "영희", age 30, company "애플"

test에는 Test Class가 할당되는데,

이 때 Test Class는 새롭게 영역을 할당해줍니다.

 

class Test {
    let testValue = 10
    let text = "반갑습니다."
    
    init() {
        hello()
    }
    
    func hello(){
        let hello = "안녕하세요."
    }
}

Test Class는 내부적으로 testValue, text 변수가 있으므로 Heap 영역에 2개의 크기만큼 할당이 되고,

MemoryExam의 test변수에 Test가 생성이 되었기 때문에

Test Class의 init() 함수가 불리고,

그로 인해 hello() 함수가 불리면서,

hello()함수 내부의 hello 변수까지 불리게 됩니다.

이 흐름 잘 따라 오셨나요?

그럼 메모리에는 

위와 같은 형태가 됩니다.

 

여기서 Test.init()과 Test.hello()은 왜 Stack인가요?라고 궁금할 수도 있습니다.

Stack이란,

함수를 호출되는 데에 필요한 메모리를 저장하는 공간인데 여기에는 지역변수, 매개변수, 리턴값 등이 함수가 Call되면 할당됩니다.

이 때 하나의 함수에 필요한 메모리 덩어리를 묶어서 Stack Frame(스택 프레임)이라고 부르는데

Xcode에서 디버깅시

이런 형태를 보신 적이 있으실거에요.

여기서 함수 단위 하나하나가 Stack Frame이고 이 전체를 통틀어 Call Stack이라고 부릅니다.

Test.init()과 Test.hello()는 call 되므로 Stack에 할당되게 됩니다.

 

여기까지 이해가 잘 되셨나요? 그럼 계속 진행해보겠습니다 :)

 

Test.hello() 함수 내부의 let hello = "안녕하세요."까지 호출되면서 Test Class의 호출이 종료되게 됩니다.

그럼 Stack에 할당되어있던 Test.hello() 영역이 pop되고, Test.init() 영역도 pop이 됩니다.

이렇게 말이죠!

그리고 Test Class 호출이 종료되면서 MemoryExam의 test 변수에는 할당된 Test Class의 주소가 저장되고,

이로인해 test 변수는 Heap에 할당된 Test 영역을 바라보게(retain) 됩니다.

 

이제 MemoryExam의 함수를 하나하나 호출해볼까요?

init() {
    run()
}
    
func run() {
    let youngHeeUser = User(name: "영희", age: 30, company: "애플")
    let copyCheolSuUser = cheolSuUser
    let copyTest = test
}

MemoryExam의 init() 함수가 처음으로 호출되고, 그 후에는 run() 함수가 호출되면서 내부 변수도 같이 메모리에 할당되게 됩니다. 

물론, 아까와 같이 초기값이 세팅되는 것이 아닌 영역만 할당이 됩니다.

이제 하나씩 실행되면서 초기값이 세팅이 되는데,

여기서 퀴즈~

copyCheolSuUser는 어떻게 메모리에 할당되게 될까요?

1. copyCheolSuUser 영역이 1개의 크기로 할당되고, cheolSuUser를 바라본다.

2. cheolSuUser와 동일한 크기로 또 메모리에 할당된다.

 

너무 쉬운 문제라 다들 아실거란 생각이 드네요 ㅎㅎ

정답은..!

 

.

.

.

.

.

.

.

.

.

.

 

2번입니다! 

 

Strcut는 동일한 Struct를 복사하면 그 영역이 copy가 된다는거 알고계시죠?!

이와 다르게 Class는 동일한 Class를 복사하면 해당 영역을 같이 바라보게(retain) 됩니다.

그래서 copyTest는 Heap에 할당된 Test 영역을 바라보게 되지요.

 

만약 이 부분을 잘 모르신다면

Swift :: 구조체와 클래스 차이 (Struct VS Class) 와

iOS :: ARC, strong, weak, unowned

를 참고하시면 됩니다.

 

그럼 위의 설명을 차근차근 진행해보면 

쨘~ 이렇게 됩니다.

어렵지 않지요???

 

이제 모든 함수가 호출되었기 때문에 pop 하는 일만 남았습니다.

 

차례대로 run() 함수가 호출 종료되었기 때문에 pop해주고,

init() 함수가 호출 종료되었기 때문에 pop 해줍니다.

이 때, copyTest가 pop되면서 바라보던게 끊어지게(release) 됩니다.

 

MemoryExam Class 호출이 종료되면서 memoryExam 변수에는 할당된 MemoryExam의 주소가 저장됩니다.

 

이후에 main() 함수 호출이 종료되면서 pop이 되고,

memoryExam이 바라보던게 끊어지면서(release) MemoryExam Class를 바라보는게 없기 때문에(retain count = 0) ARC에의해 MemoryExam도 자동으로 해제됩니다.

MemoryExam이 pop되면서 test도 pop되고, test가 바라보고 있는 것도 끊어지게 됩니다.(release)

이전과 동일하게 Test Class를 바라보는게 없으므로(retain count = 0) ARC에 의해 Test도 자동으로 해제됩니다.

모든 호출이 종료되면 처음과 같이 메모리에 아무것도 존재하지 않게 됩니다.

 

여기까지가 이론적인 Stack과 Heap의 동작방식이었는데요,

실제로는 컴파일러의 최적화 옵션에 따라 다른 모습을 보일 수도 있다는 점 기억해주세요! (사용하지 않는 변수가 사라지는 등..)

 

혹여나 잘못된 내용이 있다면 언제든지 제보 부탁드립니다!

그럼 부디 도움이 되었길 바라며,,

 

안뇽!