C# - 메모리 관리 (Stack 메모리, Heap 메모리)

이도희·2023년 5월 18일
0

C#

목록 보기
3/9

프로그램을 실행하기 위해서는 프로그램이 메모리에 로드되고 프로그램에서 사용하는 변수들을 저장할 공간도 필요하다. 따라서, 운영체제는 프로그램 실행을 위해서 프로그램에게 메모리를 할당해준다. 이 메모리 공간은 크게 데이터 영역, 코드 영역, stack 영역, heap 영역으로 나뉜다.

프로그램이 실행되면서 메모리가 어떻게 변화하는지 이해하기 위해서는 stack과 heap 영역을 살펴봐야 한다.


Stack 메모리

메모리 관리 방식 (LIFO)

stack 메모리는 stack 자료구조와 동일한 방식으로 동작한다. 자료구조 Stack은 LIFO(Last-In-First-Out) 구조로 위로 데이터들을 쌓고 (Push) 제거할 때는 최상단의 값을 빼는 (Pop) 방식이다. 스택 메모리는 이러한 이유로 비교적 시간이나 효율성 측면에서 복잡한 구조의 heap보다 이점을 가진다.

스택 메모리가 실제 프로그램 상 어떻게 동작하는지 이해하기 위해 다음의 예제를 하나 살펴보자. 코드 상 만나는 함수들을 stack에 담고, 이 함수들의 실행이 끝나면 stack에서 pop해나가는 식인 것을 확인할 수 있다.


static void Main(string[] args)
{
	int num1 = 10;
    int num2 = 5;
    int result = 0;
    result = Add(num1, num2);
    
    int Add(int num1, int num2)
    {
    	return num1 + num2;
    }
}

실행 흐름에 따른 메모리 상태
1) 먼저 Main 함수 스택 프레임이 스택 메모리에 담긴다. 이때 메인 함수에 정의된 변수들이 스택 프레임에 저장되는 것을 볼 수 있다.
2) 다음으로 Add 함수를 만나게 되면서 Add 함수 스택 프레임이 스택 메모리에 담긴다. Add 함수에는 매개 변수로 받은 변수들이 스택 프레임에 저장되는 것을 볼 수 있다.
3) Add 함수가 실행이 끝나고 스택 메모리에서 pop이 되면서 Main 함수 실행으로 돌아간다. 이때 Add 함수 스택 프레임에 있던 변수들에 대한 메모리도 해제된다.
4) 마지막으로 Main 함수 실행이 끝나고 스택 메모리에서 pop이 되면서 프로그램이 종료된다. 여기서 Main 함수 스택 프레임에 있던 변수들에 대한 메모리도 해제된다.

Stack Frame

위의 예제에서 하나의 블록 형태로 나타내고 있는 스택 프레임은 무엇일까? 스택 프레임은 함수에서 사용하는 정보들을 그룹화해놓은 것이다. 하나의 함수는 하나의 스택프레임을 가지며 스택프레임은 함수가 사용하는 지역 변수 및 매개변수, 반환값 등을 저장하고 있다.

앞서 스택 메모리에서는 이 스택 프레임을 push하고 pop하면서 함수의 실행 흐름을 제어한다. 또한 함수가 실행될 때 관련 변수들에 대한 메모리 공간이 할당된다. 이때 할당될 크기는 이미 컴파일러가 알고 있어서 해당되는 크기를 할당 받는다. 함수 실행이 끝나면 스택 프레임이 pop되면서 관련 변수들이 메모리 상에서 함께 제거된다. 이렇듯 함수가 끝날 때 가지고 있던 함수와 관련된 정보들을 제거하기 때문에 stack 메모리는 임시 메모리라고도 불린다.

스택 메모리는 함수의 실행 흐름을 제어하고, 지역 변수 및 매개 변수를 저장하고 삭제한다는 점을 이해하고 넘어가자!

Stack Overflow

한편, 스택은 스레드 당 1MB의 크기 제한을 가지고 있다. 이로 인해 만약 지정된 크기를 다 쓰고 더 사용하려고 하면 stack overflow가 발생한다. (주로 알고리즘 풀 때 재귀 호출 잘못짜면 볼 수 있다 ^^..)

❓ stack 메모리를 늘리면 해결되지 않는가?
물론 stack overflow는 해결될 수 있으나 메모리 크기가 커지면 그만큼 처리 시간이 커진다는 것도 명심해야한다. 자료구조 크기가 커지면 그만큼 탐색에도 많은 시간이 소요된다는점!

정리하면 스택 메모리는 다음의 특징을 가진다고 볼 수 있다.

1) LIFO 구조의 메모리 할당 및 해제
2) 빠르고 효율적임
3) 임시 메모리
4) 정해진 크기로 인한 stack overflow 문제

Heap 메모리

메모리 관리 방식 (Garbage Collector)

일반적으로 heap 메모리는 프로그래머에 의해 공간이 할당되고 해제된다. C#에서는 reference type의 변수나 인스턴스 변수들을 생성할 때 힙 메모리에 공간이 할당된다. C#에서는 별도의 명시가 없으면 CRL이 관리하는 힙으로 managed heap을 가리키는데 이에 대한 메모리 해제는 garbage collector가 알아서 해준다.

1) Value Type : 값 자체를 들고 있는 형식
2) Reference Type: 저장하고자 하는 본체의 메모리 주소를 가지는 형식

메모리 할당

프로세스 별 -> managed heap 인접한 공간에 주소를 할당 받는다. 이때 stack과 유사하게 heap pointer를 하단에 두고 object 할당하면 더하는 식으로 진행하기 때문에 상당히 효율적으로 동작한다고 볼 수 있다. (일반적으로 살펴볼 때는 스택보다 비효율적이라고 이야기하지만 동작 자체는 꽤나 효율적으로 하고 있다는 점!)
스택처럼 최상단에만 접근하는 것이 아닌 어떤 위치의 item이든 접근할 수 있지만 그만큼 더 복잡한 구조를 가지고 있기에 stack과 비교했을 때 비효율적인편이다. 한편, 메모리 크기 제한 측면에서는 stack보다 이점을 가지고 있다.

❓ reference type의 변수는 항상 heap 메모리를 가리키는가?
reference type이 100% heap을 가리킨다고 할 수는 없다. 함수 호출시 ref type을 붙여서 struct를 참조하는 경우 주소를 넣어 동작하게 된다. 이 경우 실제 struct 본체는 value type이라 stack에 있으므로 함수가 받은 reference type이 제일 위의 stack을 가리킬 수 있다.

❓ heap 메모리와 heap 자료 구조는 동작 방식이 연관 있는가?
스택과 달리 heap 메모리는 heap 자료 구조와는 직접적인 연관이 없다. 단지 프로그래머가 할당 및 해제하는 메모리 구조 collection이라 heap이라는 명칭을 사용하는 것이다.

그럼 heap은 프로그램이 실행될 때 메모리 상 어떻게 관리될까? 다음의 인스턴스 변수가 생성되는 과정을 볼 수 있는데, heap 메모리에는 본체 자체가 들어가 있고, stack에는 본체를 가리키는 주소가 들어있는 것을 확인할 수 있다.

public Class Animal
{
    int weight;
    string name;
    public Animal(int weight, string name)
    {
        this.weight = weight;
        this.name = name; 
    }
}

class Program
{
	static void Main(string[] args)
    {
    	int weight = 10;
        Animal testAnimal = new Animal("cat", weight);
    }
}

실행 흐름에 따른 메모리 상태

1) Main 함수가 실행되면서 해당 스택 프레임이 스택 메모리에 쌓인다. 이때 함수 내의 변수도 차례로 스택 프레임에 저장되는 것을 볼 수 있다.
2) Animal 인스턴스 생성을 시도하는데 여기서 생성자 함수를 호출하기 때문에 해당 생성자 함수의 스택 프레임이 스택 메모리에 쌓인다. Animal 생성자 함수 스택 프레임에는 매개 변수로 들어온 변수들이 함께 저장된 것을 볼 수 있다. (여기서 Animal 생성자 함수가 끝나면 stack에서 pop된다.)
3) 힙 메모리에 Animal 인스턴스를 생성한다 (new 키워드). 생성자에 들어간 값들이 인스턴스에 들어가고 최종적으로 인스턴스를 가리키는 주소 값이 Main 함수의 Animal 변수에 저장된다.

(+ string 자체도 reference type 변수라 heap에 인스터스 두고 주소를 가리키는 식으로 표현해야 하는데 편의상 그냥 표현했다.)

정리하면 힙 메모리는 다음의 특징을 가진다고 볼 수 있다.

1) Garbage Collector에 의한 메모리 해제
2) 비교적 비효율적임
3) life time이 김 (스택처럼 함수 끝나고 메모리 날아가는게 아님)
4) 메모리 크기 제한이 없음

heap 메모리와 관련된 garbage collection은 다음 포스트에서 더 자세히 다루고자 한다.

profile
하나씩 심어 나가는 개발 농장🥕 (블로그 이전중)

0개의 댓글