CS

[CS] 스택(Stack)과 힙(Heap) 차이

usingsystem 2022. 8. 10. 10:43
728x90

Stack (스택) 이란?

- 프로그램을 실행하는데 필요한 메모리 공간으로 메소드가 호출되는데 필요한 메모리는 스택에 저장된다.

여기서 이야기하는 메소드가 호출되는데 필요한 메모리는 지역변수, 매개변수, 리턴값 등이다.

 

Stack Frame (스택프레임)

- 메소드를 호출하기 전에 반드시 호출에 필요한 메모리를 스택에 만들어야 하는데,

이 때 하나의 메소드에 필요한 메모리 덩어리를 묶어서 '스택프레임' 이라고 한다.

즉, 하나의 메소드에는 하나의 스택프레임이 존재하는 것이다.

스택프레임의 구성 요소로는 위에서 이야기한 지역변수, 매개변수, 리턴값 등이 있다.

 

메소드 호출과 스택메모리

- 하나의 메소드를 호출하면 그 메소드에 해당하는 하나의 스택프레임이 스택에 만들어진다.

스택에 만들어진 메모리는 메소드의 호출이 끝나는 순간 스택메모리에서 제거된다.

스택이라는 것은 기본 구조가 Last In First Out (LIFO) 즉, 마지막에 들어간 것이 가장 먼저 나오는 구조이다.

예를들어, Main() 안에 Calc() 라는 메소드가 있고, Calc() 안에 Sum() 이라는 메소드가 있다고 가정하자.

프로그램이 실행되면 가장 먼저 Main() 이 스택에 저장되고, 그 다음에 Calc(), Sum() 이 순차적으로 실행되며 스택에 저장된다.

그렇다면 세개의 메소드 중 어느 것이 가장 먼저 실행이 끝날까? 그것은 말할것도 없이 바로 Sum() 이다.

스택메모리는 메소드의 호출이 끝나는 순간 해당 메소드에 관한 메모리를 제거하므로

Sum() 이 가장 먼저 스택에서 제거된다. 그 다음은 Calc(), Main() 순이다.

 

 

위 그림처럼 메소드가 스택에 저장되고, 제일 위에 있는 Sum()부터 메모리에서 제거된다.

 

Heap (힙) 이란?

- C#에서의 메모리 구성은 스택과 힙으로 구성된다. 힙은 스택에 반대되는 개념의 메모리이다.

위에서 이야기 했듯이 스택에는 지역변수, 매개변수, 리턴값 등 변수들이 저장되고,

힙에는 new를 이용해서 생성한 메모리가 저장된다.

즉, 한줄로 요약하면 메소드 내에서 선언된 변수는 스택에 저장되고 new를 이용해서 생성한 메모리는 힙에 저장되는 것이다.

 

 

위 그림은 간단한 코드를 예로 어떤 것이 스택에 저장되고, 어떤 것이 힙으로 저장되는지 나타낸 것이다.

빨간색 동그라미 안에 있는 것이 스택에 저장되는 것이고, 파란색 동그라미 안에 있는 것이 힙에 저장되는 것이다.

아래 exStackHeap 클래스를 ex 라는 이름으로 생성한 것을 보면 스택과 힙에 각각 저장 된 것을 볼 수 있다.

'ex' 는 메소드 내에 있는 x, y와 마찬가지로 변수이기 때문에 스택에 저장된다.

하지만, ex 라는 이름으로 생성되는 exStackHeap 클래스의 내용은 힙에 저장된다.

즉, 변수 명 자체는 스택에 저장되고 그 변수가 참조하는 내용은 힙에 저장되는 것이다.

때문에 위의 코드에서 32비트 운영체제 기준으로

스택에 x, y, ex 세개의 변수가 각각 4바이트씩 총 12바이트의 메모리가 저장되고

힙에 exStackHeap를 참조로 생성하여

exStackHeap 클래스가 갖고 있는 a, b 두개의 변수가 각각 4바이트씩 총 8바이트의 메모리가 저장된다.

 

만약 exStackHeap 클래스가 가지고 있는 변수 a, b가 int[] a = new int[5] 이런식으로 new를 이용해 생성된다면 어떻게 될까?

정답은 a 와 a가 갖고있는 5개의 정수 모두 힙에 저장 된다는 것이다.

a, b는 힙에 저장되고 a가 가지고 있는 5개의 정수는 힙 공간내 다른곳에 저장된다. 즉, 힙의 힙 인것이다.

 

Heap Memory (힙 메모리) 제거

- 스택은 메소드의 호출이 끝나는 순간 메모리가 제거된다. 즉, 메소드의 중괄호가 닫히는 곳에서 메모리가 제거 되는 것이다.

그렇다면 힙 메모리는 언제 제거 될까?

C# 에는 Garbage Collector (가비지 콜렉터) 라는 것이 있어 가비지 콜렉터가 알아서 힙 메모리에 있는 것을 제거해준다.

가비지 콜렉터는 계속해서 힙 메모리를 지켜보며

사용자가 더 이상 사용하지 않는 메모리나 불필요한 메모리

(참조가 가리키고 있는 데이터로 힙에 저장 되었지만

스택에서 참조변수가 제거되어 더이상 그 데이터를 엑세스할 수 있는 참조 변수가 없는 상황 등) 를

제거하고 메모리가 부족하다고 판단되면 메모리 조각 모음까지 해준다.

(가비지 콜렉터가 제거해 주지 않는 이상 참조변수가 스택에서 제거되어도 데이터는 힙에 계속 남아있다) 

 

new 로 생성한 모든 객체의 메모리는 힙에 만들어 지는 것과 동시에 가비지 콜렉터의 관리 대상이 되어 사용자가 신경쓸 필요가 없다.

하지만 꼭 굳이 사용자가 힙 메모리를 정리하고 싶다면 GC.Collect() 를 이용해서 가비지 콜렉터를 강제 호출 할 수 있다.

오늘은 스택과 힙메모리의 차이에 대해서 알아보도록 하겠습니다. C#에서도 당연히 스택기반의 메모리와 힙 기반의 메모리를 제공합니다. C#에서 이 메모리들의 구조를 확인하기 위해서는 먼저 값 형식(Value Type)과 참조 형식(Reference Type)에 대해서 알고 있어야합니다. 값 형식과 참조 형식에 대한 정의는 아래와 같습니다.

 

  • 값 형식 : 값을 변수에 넣는 데이터 형식
  • 참조 형식 : 변수에 대한 위치(메모리 위치)를 담는 데이터 형식

 

간단하게 값 형식과 참조 형식에 대해서 알아보았습니다. 그럼 변수를 값에 넣을 때 메모리 구조에서 어떻게 처리되는지를 알아 보겠습니다. 먼저 값 형식을 통해서 변수에 값을 넣게 되면 스택 메모리에 변수의 값이 저장됩니다.

 

그럼 스택 구조를 먼저 알아보도록 하겠습니다. 스택은 컴퓨터 사이언스 전공에서 배우는 자료구조의 한 종류입니다. 전형적인 LIFO(Last In First Out)구조로써, 가장 나중에 들어온 데이터가 자료구조에서 가장 먼저 나가는 구조로 이루어 져 있습니다. 이러한 구조를 한국말로는 후입선출이라고 부르고 있습니다. 이와 반대되는 자료구조는 FIFO(First In First Output)구조로 대표적인 예로 링크드리스트(Linked List)가 있습니다. 

 

 

 

 

 

스택 구조가 실질적으로 코드상에서 어떻게 처리가 되는지 알아볼까요. 아래와 같은 간단한 코드가 있다고 가정을 해 봅시다.

 

public void Init()

{

 

int a  = 10; // 1번 과정

int b = a;

b = 20; // 2번 과정

a = 30;

 

} // 3번 과정

 

1번 과정을 통해서 정수형의 a, b가 생성이 됩니다. a, b는 스택 메모리의 어딘가에 할당이 되며 초기화한 값이 할당된 곳에 저장이 됩니다. 그림으로 나타내면 아래와 같습니다. 

 

 

 

2번 과정이 진행되면, 1번 과정에서 할당된 각각의 위치에 값이 할당이 됩니다. 

 

 

 스택메모리를 이용하면 변수가 할당 된 위치에 값만 변경이 되는 것입니다. 그리고 마지막으로 3번 과정을 거치게 되면 할당된 a, b가 역순대로 메모리에서 자동적으로 정리가 됩니다. 

 

값 형식을 통해서 메모리에 데이터를 넣게 되면, 스택 메모리를 사용하기 때문에 LIFO 방식으로 데이터가 없어진다고 설명드렸습니다. 그럼 값 형식에 할당된 데이터가 사라지는 시점은 언제일까요? 그 시점은 위의 코드에서 3번 과정 }를 만날 때 메모리에 할당된 데이터가 정리가 됩니다. 스택 메모리는 전형적으로 변수가 선언된 Scope를 벗어나게 되면 할당이 해제가 됩니다. 

 

스택 메모리에서 중요한 점은 1) 할당된 위치에 값만 지속적으로 변경되는 점 2) LIFO 방식이며, 3) 해제시점은 변수선언 영역을 벗어나는 때라는 것만 이해하고 계시면 됩니다. 

 

그럼 지금부터는 참조 형식의 변수 할당은 어떻게 되는지 알아보도록 하겠습니다. 참조 형식의 변수 할당이 어떻게 되는지 알아보기 위한 예제코드는 아래와 같습니다.

 

class Program

{

    public int value;

    static void Main(string[] args)

    {

        Program prog = new Program(); // 1번과정

        prog.value = 100;

 

        Program prog1 = prog; // 2번 과정

 

        prog1.value = 200; // 3번 과정

    }

}

 

위의 코드 1번과정을 거쳐서 prog 객체를 생성하고, 이에 대한 값을 100으로 설정합니다. 이렇게 되면 스택에는 prog에 대한 메모리 주소가 들어가게 됩니다. 그리고 힙에는 실질적인 값이 들어갑니다.

 

이 말의 의미는 prog는 5000번지를 참조하고 있는데, 5000번지에 저장된 값이 100이라는 의미입니다. 

 

 

 

2번 과정을 통해서 prog1 객체에 prog 객체를 넣습니다. 이러한 경우 단순히 Stack에서의 복사만 이루어 집니다. 스택메모리에는 Prog1에 대한 변수가 할당이 되지만, Stack에 들어있는 값은 prog가 참조하고 있는 주소인 5000이 들어갑니다. 따라서 prog, prog1 변수는 둘다 5000번지에 있는 값 100을 참조하고 있는 것입니다. 이해해야 하는 포인트는 값이 복사가 되는게 아니라 주소가 복사가 된다는 점 입니다.

 

3번 과정을 통해서 prog의 값을 200으로 변경합니다. 값을 변경하면 힙 메모리에 저장된 값이 변경이 되겠죠. 현재 prog와 prog1은 모두 동일한 메모리를 참조하고 있습니다. 따라서 prog와 prog1의 value 값은 모두 200으로 변경이 됩니다. 

 

힙 메모리에서 중요한 점은 1) Stack에는 메모리 주소가, Heap에는 실질적인 값이 들어간다는 점 2) 새로운 객체 변수에 기존의 변수를 할당하면 Stack의 메모리 주소만 복사된다는 점 3) 값을 변경하면 값은 메모리를 참조하고 있는 모든 변수의 값은 동일해 진다는 점을 이해하고 계시면됩니다. 

 

스택 메모리는 해당 함수가 종료되면 할당된 값이 없어진다고 설명드렸습니다. 그럼 힙 메모리 영역은 언제 없어질까요? 이에 대한 답은 알 수 없다 입니다. 스택 메모리와 달리 힙 메모리 기반은 Garbage Collector(GC, 가비지 콜렉터)라는 녀석이 알아서 할당을 해제합니다. 내부적으로 할당 해제 알고리즘이 있긴 하지만 개발자 및 사용자 입장에서는 알 필요가 없습니다. 그 이유는 CLR에서 알아서 해제를 해주기 때문입니다.

 

CLR의 GC는 기존의 C/C++과 C#의 가장 큰 차이점입니다. C/C++에서는 메모리 할당 및 해제를 개발자가 알아서 처리해줘야 했습니다. 그러지 않으면 Memory Leak(메모리 릭)이 발생을 하여 메모리 처리 부분에 큰 문제점을 야기하였습니다. Java에서 부터 큰 인기를 얻은 GC를 C#에도 도입함으로써 개발자의 오류를 줄여주는 역할을 하고 있습니다. 하지만 GC가 있으면 성능상으로 느려진다는 단점도 가지게 됩니다. 

 

728x90

'CS' 카테고리의 다른 글

[CS] 컴파일(Compile)이란?  (0) 2022.08.19
[CS] 프로그래밍 언어와 빌드 과정 [Build Process]  (0) 2022.08.19
[CS] 메모리구조  (0) 2022.08.11