본문 바로가기
개린이 이야기

메모리에 관하여

by iOS 개린이 2022. 9. 25.

메모리 구조

-메모리는 연산을 수행하는 CPU를 보조하는 역할로 입력값, 함수, 출력값 등을 저장한다. (변수와 상수, 함수, 리턴값 등)

 

 -메모리는 물리적으로 하드디스크, 램, 레지스터, 캐쉬로 구성되는데 각각 특성에 따른 비용과 속도가 다르고, 이에 따라 용도도 다르다.

그럼 개발자가 변수나 함수 등을 어느 메모리에 저장해야 하는지 지정해주어야 하나? 

-> 아니다. 운영체제가 물리적인 메모리를 가상 메모리로 만들어 관리하기 편하게 해준다.

 

-프로그램이 실행되면 운영체제(OS)에서 이 프로그램을 위한 가상 메모리 공간을 할당해준다.

공간은 1. 코드 영역, 2. 데이터 영역, 3. 힙 영역, 4. 스택 영역으로 나누어져있다.

 

-우리가 메모리를 이해하는 것은 성능 최적화, 메모리 누수 방지, 버그 해결 등 다양한 이유로 중요하다.

특히, 참조 타입과 값타입을 이해하려면 이들이 저장되는 힙 영역과 스택 영역에 대해 이해하는 것이 중요하다.

 

 

1. 코드 영역

-코드영역 또는 텍스트 영역이라고도 불린다. 이 영역은 실행 가능한 명령어들이 저장되는 곳이다.

컴파일 타임에 우리가 작성한 소스 코드(고급언어)를 기계어 형태(컴파일을 통해 고급언어 -> 기계어(이진법))로 저장하고 있다.

읽기 전용으로 설정되어 있어서 프로그램이 실행되는 동안 이 영역의 데이터가 변경되는 것을 방지한다.

 

-코드 영역에 저장된 기계어는 프로그램이 실행될 때, CPU가 읽고 해석하여 명령어를 수행하게 된다.

 

-코드 영역은 메모리 공간에서 가장 낮은 주소에 위치하며, 이 영역의 크기는 프로그램의 크기에 따라 결정된다.

 

2. 데이터 영역

-데이터를 저장하는 영역으로 전역 변수와 정적(static) 변수가 저장된다. (프로그램이 종료될 때까지 쓰일 변수들.)

데이터 영역은 주로  '초기화된 데이터 영역', '초기화되지 않은 데이터 영역' 두 가지 섹션으로 나뉜다.

 

초기화된 데이터 영역

이 영역은 프로그램의 시작 시점에 이미 값이 할당된 전역 변수와 정적 변수를 포함한다.

예를 들어, 프로그램 내에서 'let globalVar = 10' 이라는 전역 변수를 선언했다면, 이 변수는 초기화된 데이터 영역에 위치한다.

 

초기화되지 않은 데이터 영역

이 영역은 프로그램 시작 시점에 값이 할당되지 않은 전역 변수와 정적 변수를 포함한다.

예를 들어, 'var globalVar: Int' 라는 전역 변수를 선언만 하고 값을 할당하지 않았다면, 이 변수는 초기화되지 않은 데이터 영역에 위치한다. 이 영역의 변수들은 프로그램이 시작될 때 모두 0으로 초기화된다.

 

데이터 영역의 변수들은 프로그램의 실행이 끝날 때까지 메모리에 유지되며, 프로그램 어디에서든 접근이 가능하다.

따라서, 전역 변수나 정적 변수를 사용할 때는 주의가 필요하다. 왜냐하면 예상치 못한 변경이 발생할 수 있기 때문이다.

 

3. 힙 영역

-힙 영역은 프로그래머가 직접 메모리를 할당하고(동적 할당), 해제할 수 있는 영역이다. 

Swift에서 힙 영역에 주로 할당되는 것들은 클래스의 인스턴스, 클로저 등 참조타입 객체가 힙에 할당된다.

또한, 데이터의 크기가 확실하지 않아 실행 중에 크기가 변경될 수 있기 때문에 메모리 영역 중 유일하게 런타임에 결정된다.

 

Swift에서는 ARC(Automatic Reference Counting)를 통해 힙에 있는 객체의 메모리를 자동으로 관리해준다. 객체에 대한 참조가 더 이상 없을 때, ARC는 해당 객체를 힙에서 제거한다.

 

힙 영역의 주의할 점이 있다. 동적 메모리 관리는 프로그램의 복잡성을 증가시킬 수 있다. 

따라서, ARC가 해결해주지 못하는 순환참조 문제나 객체의 수명주기 체크 오류 등으로 인해 메모리 릭이 발생하거나, 원하는 결과를 얻지 못할 수 있다.

 

 

4. 스택 영역

-스택 영역은 프로그램의 함수 호출과 관련된 메모리를 저장하는 공간이다.

함수 호출 시, 해당 함수의 매개변수, 지역 변수, 반환 주소 등이 스택에 할당되며, 이러한 정보는 함수의 종료와 동시에 해제된다. (함수 수행 중에 잠깐만 필요한 데이터들이 저장됨.)

Swift에서는 값 타입인 구조체나 열거형 등을 스택에 저장한다.

 

스택 영역은 크게 LIFO 구조, 메모리 관리의 간편함, 수명주기 특징을 가지고 있다.

 

LIFO(Last In First Out)

스택은 가장 마지막에 들어간 데이터가 가장 먼저 나오는 LIFO 구조를 가지고 있다.

즉, 가장 최근에 호출된 함수의 데이터가 스택의 상단에 위치하게 되고, 이 함수가 호출되면 메모리에서 해제된다.

 

메모리 관리의 간편함

스택 영역은 자동으로 관리되므로, 프로그래머가 직접 메모리를 관리할 필요가 없다.

 

수명주기

스택에 저장되는 데이터는 해당 함수의 수명주기에 종속적이다. 함수가 종료되면, 해당 함수에서 사용되던 메모리는 자동으로 해제된다.

 

스택은 한정적인 영역을 가지고 있기 때문에 너무 큰 메모리를 할당하면 스택 오버플로우가 발생한다.

스택 오버플로우는 스택이 너무 많은 데이터로 인해 용량이 초과될 때, 발생하는 문제이다.

따라서 데이터의 크기와 수명주기를 고려하여 스택과 힙을 적절하게 사용해야 한다.

 

 

힙과 스택 영역

 

스택 영역 장점

 -스택 영역의 장점은 메모리 관리가 자동으로 이루어진다는 점이다. (관리해줄 필요가 없어서 편함.)

함수의 수명주기와 함께 모든 메모리는 자동으로 제거되기 때문에 메모리 누수의 위험이 적다.

 

-스택에 저장된 데이터는 직접 접근이 가능하기 때문에 접근 속도가 빠르다.

이 뜻은 스택에 저장된 변수는 해당 변수가 선언된 함수의 위치를 통해 알아내며, 컴파일 시점에 이미 결정된다.

따라서, 런타임에 해당 변수의 위치를 찾는 데 추가적인 연산이 필요하지 않다.

 

반면에 힙에 할당된 데이터는 메모리 주소가 런타임에 결정된다.

이로 인해 데이터에 접근하기 위해 먼저 메모리 주소를 찾아야 하므로 상대적으로 시간이 더 걸릴 수 있다.  

 

스택 영역 단점

-스택의 크기는 컴파일 시 결정되며, 런타임에 변경할 수 없다.

따라서 정해진 스택의 크기를 초과하여 데이터를 쌓으면, 스택 오버플로우가 발생하게 된다.

 

-스택은 저장되는 데이터가 함수의 수명주기에 종속되기 때문에 외부에서 해당 데이터에 접근할 수 없다.

이것은 안정성에서는 장점이 되지만, 유연성의 관점에서는 단점이 될 수 있다.

 

 

힙 영역 장점

-힙 영역에 저장된 데이터의 수명주기는 개발자가 컨트롤 할 수 있다. (원하는 상황에 메모리를 해제시킬 수 있다.) 

이것은 다른 함수의 수명주기와 독립적으로 데이터를 관리할 수 있는 경우에 유용하다.

 

-힙 영역은 스택 영역보다 크기 때문에 대용량 데이터나, 객체, 배열 등을 저장하기에 적합하다.

스택에서 큰 메모리를 할당하려고 하면 스택 오버플로우가 발생할 수 있음.

 

힙 영역 단점

-힙 영역은 스택 메모리를 사용하는 것보다 시간과 리소스가 더 많이 사용된다.

왜냐하면 힙에서 메모리를 할당하고 해제하는 과정이 스택에 비해 복잡하기 때문이다. 

 

-힙의 유연성은 메모리를 해제하지 않거나, 해제한 메모리에 다시 접근하는 등의 메모리 오류를 겪을 수 있다.

메모리를 직접 관리해줘야 함.

 

 

힙과 스택 영역의 사실

-힙 영역과 스택 영역은 같은 메모리 공간을 공유한다.

힙 영역은 낮은 메모리 주소부터 할당받고, 스택 영역은 높은 메모리 주소부터 할당받는다.

따라서, 스택 영역이 커짐으로 힙 영역을 침범하면 스택 오버플로우,

반대로 힙 영역이 스택 영역을 침범하면 힙 오버플로우가 발생한다.

 

스택 오버플로우 예시 = 무한 루프 함수

힙 오버플로우 예시 = 관리되지 못하고 남아있는 불필요한 메모리들(메모리 릭)

 

값 타입과 프로토콜

 

-Swift에서 값 타입은 일반적으로 스택 영역에 할당되지만,

값 타입이 프로토콜을 채택하면 일반적인 값 타입의 규칙이 적용되지 않을 수 있다.

 

프로토콜은 참조 타입과 값 타입 모두 채택할 수 있다.

이로 인해, 같은 프로토콜을 채택하는 참조 타입과 값 타입이 있을 때, 해당 프로토콜 타입을 변수나 함수의 인자로 받으면 

어떤 타입이 할당될 지 컴파일 타임에 결정할 수 없을 수 있다.

 

만약 컴파일 타임에 프로토콜 타입의 변수나 함수 인자가 실제로 어떤 타입의 값을 가질 지 미리 알 수 없다면,

런타임에 동적으로 메모리를 관리해야 한다. 이 경우, 값 타입의 인스턴스도 힙에 할당될 수 있다.

 

여기서 중요한 것은 "이것은 값 타입이니까 무조건 스택 영역에 할당되겠구나" 라는 생각을 버리고,

코드를 유연하게 이해하려고 노력하는 것이다.

원리에 대해 깊이 이해하면, 메모리 관리와 동작 방식에 대해 더 잘 이해할 수 있을 것이다.

 

 

 

 

 

 

 

 

참고: 

https://m.blog.naver.com/PostView.nhn?blogId=jdub7138&logNo=220928509246&proxyReferer=https:%2F%2Fwww.google.com%2F

iOS) 메모리 구조 (Code, Data, Stack, Heap) (tistory.com)

[Swift] 메모리 구조 (velog.io)

[Swift][OS] 메모리 구조에 대하여 (velog.io)

iOS) 메모리구조 - Code, Data, Stack, Heap – 유셩장 (sihyungyou.github.io)

[Swift] 메모리 구조 (tistory.com)