메모리영역에는 크게 4가지 영역이 존재하며 Code, Data, Heap, Stack 영역이 존재합니다.
Code 영역
우리가 작성한 코드는 컴파일 과정을 거쳐 기계어 형태(컴퓨터가 읽을 수 있는 0과 1로 이루어진 형태)로 변환되어 이 영역에 저장됩니다.
CPU는 이 영역에 저장된 코드를 읽고 해당 작업을 처리합니다.
프로그램 실행과 동시에 메모리에 할당되며, 프로그램이 종료되면 메모리에서 해제됩니다.
이 영역은 Read-Only(읽기 전용) 으로 실행중에는 변경할 수 없습니다.
Data 영역
전역변수와 정적(static)변수가 저장되는 메모리 영역입니다.
프로그램 실행 시 메모리에 할당되며, 프로그램 종료 시 메모리에서 해제됩니다.
프로그램 실행 중에 변수 값이 변경될 수 있으므로 Read-Write(읽기-쓰기가 가능) 입니다.
Heap 영역
class 인스턴스나 클로저와 같은 참조 타입(reference type)의 데이터가 할당되는 동적 메모리 영역입니다.
이 영역의 메모리 크기는 런타임에 결정되며, 확정되지 않은 크기의 데이터가 저장됩니다.
ARC를 통해서 메모리 관리가 자동으로 이루어집니다.
개발자는 메모리 누수에 주의해야 합니다.
자동으로 관리하지만 관리를 해야합니다.
메모리에 할당되고 해제되는 과정이 상대적으로 느립니다.
Stack 영역
함수 호출 시 생성되는 지역변수, 매개변수, 리턴값 등이 저장됩니다.
일반적으로 값 타입(value type)의 데이터가 저장됩니다.
함수가 종료되면 자동으로 메모리에서 해제됩니다.
메모리 할당 및 해제의 속도가 Heap 영역보다 빠릅니다.
컴파일 시점에 해당 영역의 메모리 크기가 결정됩니다.
스택 메모리는 크기가 제한되어 있어, 이를 초과하면 스택 오버플로(Stack Overflow) 오류가 발생하여 프로그램이 종료될 수 있습니다.
💡 class의 인스턴스가 let인데 프로퍼티를 변경할 수 있었던 이유!
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
let person = Person(name: "Brody", age: 30)
print(person.name) // 출력 : Brody
person.name = "Change Name"
print(person.name) // Change Name
// let person인데 이름을 변경할 수 있는 이유!?
// class의 인스턴스는 reference type이여서 메모리의 주소값을 저장하고 있죠?
// person에는 인스턴스의 실제 주소값이 있을거에요 (ex 0x0016)
// 이름을 변경하면 새로운 인스턴스를 생성하지 않기 때문에 새로운 주소값으로 변하지 않아요!
// 그래서 참조타입의 인스턴스의 값은 let이여도 바꿀수 있었던거죠!
// class는 주소값을 저장하기 때문에 한곳에서 변경해도 모두 변경됩니다.
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
let person = Person(name: "Brody", age: 30) // person : 주소값 0x0001을 저장
let person2 = person // person2 : 주소값 0x0001을 저장
person2.name = "John" // person2를 변경합니다. 주소값 0x0001의 name을 변경합니다.
print(person.name) // John
print(person2.name) // John
func nameChange(person: Person) {
person.name = "바뀐이름"
}
nameChange(person: person) // person의 주소값 전달!
print(person.name) // "바뀐이름"
print(person2.name) // "바뀐이름"
// struct은 value type이기 때문에 값을 복사하여 전달합니다.
// 전달받은 값을 변경해도 원본이 변경되지 않습니다.
struct Animal {
var name: String
var age: Int
}
var animal = Animal(name: "Dog", age: 3)
var animal2 = animal
animal2.name = "Cat"
print(animal.name) // "Dog" 원본의 값은 변경되지 않아요.
print(animal2.name) // "Cat"
func changeName(animal: Animal) {
var animal = animal
animal.name = "Bird"
}
changeName(animal: animal)
print(animal.name) // "Dog" 원본의 값은 변경되지 않아요.
print(animal2.name) // "Cat"
🛠️ Copy On Write 알아보기
📚 메모리 효율을 위해서 value type 의 인스턴스를 다른 값에 할당하거나 파라미터로 전달할 때 값이 변경되기 전까지는 실제로 복사하지 않는 것을 의미합니다.
// 메모리를 확인하는 함수!
/*
func address(of object: UnsafeRawPointer) -> String {
let address = Int(bitPattern: object)
return String(format: "%p", address)
}
*/
func address(of object: UnsafeRawPointer) -> String {
return String(describing: object) // 포인터 주소를 문자열로 출력
}
var a = [1,2,3]
var b = a
print(address(of: &a)) // 0x60000171d6a0
print(address(of: &b)) // 0x60000171d6a0
// value type인 배열 a를 b에 할당하고 주소값을 확인했는데 똑같은 주소값이네요!
// 값을 작성하기 전까지는 동일한 메모리를 사용하는것을 의미합니다!
b.append(100)
print(address(of: &a)) // 0x60000171d6a0
print(address(of: &b)) // 0x6000021014c0
// b 배열에 값을 작성하면 그때 메모리를 복사합니다.
// 이렇게 하는 이유는 메모리 효율을 높이기 위해서입니다!
// WRITE를 하기전까지는 쓸모없는 메모리를 사용하지 않죠!
var c = a
c = [1,2,3]
print(address(of: &a)) // 0x60000171d6a0
print(address(of: &c)) // 0x60000170d920 //동일한 값을 넣었어도 값을 수정한거기 때문에 새로운 주소값이 나옵니다!