-
[자료구조] 메모리 구조와 동적 할당 - 프로그래밍 언어별로 어떻게 다를까? - C/C++/Java/Python| 자료구조 & 알고리즘/자료구조 2021. 6. 16. 11:28
프로세스 메모리 구조
먼저 메모리 구조를 살펴볼 필요가 있겠습니다. 기본적인 메모리 구조는 컴퓨터 과학의 영역이기 때문에 프로그래밍 언어별로 다르지 않고 모두 이 구조를 따릅니다. 다만 가상머신을 경유하는 언어들의 경우 가상머신에서 이 구조를 다르게 구분하여 사용하는 경우는 있습니다. 이는 아래에서 다루겠습니다.
프로그램이 프로세스되면 크게 코드 영역(text)과 데이터 영역(data + bss)으로 나뉩니다.
[1] 코드 영역
코드 영역은 말 그대로 수행될 명령어가 자리잡는 곳으로, 프로세스가 실행할 코드와 매크로 상수가 기계어의 형태로 저장된 공간입니다. 컴파일 타임(Compile Time)에 결정되고 중간에 코드를 바꿀 수 없게 READ-ONLY로 지정되어 있습니다.
[2] 데이터 영역
데이터 영역은 상수 영역, 정적 영역이라고도 합니다. 프로그램의 실행과 함께(Runtime) 생성되는 영역이며, 상수, 리터럴(literal), 정적(static) 변수, 전역 변수가 자리잡게 됩니다. 이들은 실행 도중 변할 수 있으므로 READ/WRITE로 지정되어 있습니다.
프로그래머들이 static 변수나 전역 변수의 사용을 지양하는 이유는 해당 변수들은 프로그램이 종료되기 전까지 유지되는 영역이기 때문에 CPU를 잡아먹고, 다른 프로그램과의 충돌이 일어나기 쉽기 때문입니다.
초기화되지 않은 데이터들은 bss 영역에, 초기화된 데이터들은 data 영역에 자리잡게 됩니다.
프로그램이 한 줄씩 진행되며 사용되어지는 메모리에는 크게 힙 메모리(Heap Memory)와 스택 메모리(Stack Memory)가 있습니다.
[3] 스택 메모리
스택 메모리는 컴파일 타임에 크기가 결정되며 지역변수, 매개변수, 반환값 등이 저장되는 공간입니다. 기록하고 종료하는 매커니즘은 FILO LIFO - 선입후출(First In Last Out) 후입선출(Last In First Out) -를 따르므로 데이터를 push - pop 하며 사용하게 됩니다. 이는 스택 메모리의 주소가 내림차순(높은 주소에서 낮은 주소)의 방향으로 할당되기 때문입니다.
스레드마다 별도로 존재하며 스레드가 시작될 때 할당됩니다. 또한 함수가 호출될 때 생겨나고 함수가 종료될 때 저절로 사라지는 공간이므로 특별히 메모리를 수거하거나 지우는 작업은 필요하지 않습니다. 각 함수, 스레드는 다른 함수, 스레드의 스택 메모리에 놓여진 값들을 참조할 수 없습니다.
stack overflow가 일어나는 경우는 크게 두가지로 나눌 수 있습니다.
1) 재귀함수를 잘못 설계했거나 주어진 데이터가 너무 커서 재귀함수의 depth가 너무 깊어지게 될 때, stack 메모리가 허용치를 초과하기 때문에 stack overflow가 발생하고 컴파일 에러가 발생합니다.
2) 함수 내에 지역변수가 너무 많을 경우 stack overflow가 발생하고 컴파일 에러가 발생합니다.
[4] 힙 메모리
힙 메모리는 런타임에 크기가 결정되며 프로그래머는 언어별로 특정 메서드들을 통해 힙 메모리를 할당받아 사용하게 됩니다. 모든 스레드에서 공유하며, 프로그래머는 다음 경우에 힙 메모리를 할당받아 사용하게 됩니다.
1) 데이터 배열의 크기가 일정하지 않거나 변동이 있을 때
2) 프로그램 전반에서 해당 주소를 참조할 필요가 있을 때
다만 할당받아 사용한 메모리는 반드시 해제해야 합니다. 메모리가 해제되지 못하면 메모리 누수(memory leak)가 발생하는데,
구천을 떠도는 영혼이 되어컴파일과 실행 자체에는 크게 문제되지 않지만, 누적될 시 CPU를 지연시키고 자타 프로그램 사용시 힙 용량이 모자라 프로그램이 작동되지 않는 에러가 생길 수 있습니다. 이런 경우 프로세스가 종료되면(시스템 종료) 운영체제(OS)에 메모리 리소스가 반납되며 해제됩니다. => 재부팅 시 프로그램 작동 에러들이 대부분 해결되는 이유임베디드 시스템처럼 메모리가 크지 않은 경우 시스템 신뢰성에 크게 문제될 수 있습니다.
힙 메모리는 스택 메모리보다 사용할 수 있는 메모리 공간이 크고 사용이 유연하다는 장점이 있지만, 포인터로서 메모리에 접근하기 때문에 (한 단계를 더 거친다는 점에서) 데이터의 READ/WRITE가 비교적 느리다는 단점이 있습니다.
스택 메모리와 반대로 FIFO LILO - 선입선출 후입후출 -를 따릅니다. 이는 힙 메모리의 주소가 오름차순(낮은 주소에서 높은 주소)의 방향으로 할당되기 때문입니다.
.
.
.
동적 메모리 할당(Dynamic Memory Allocation)이란?
프로그램의 실행 시간(Runtime)동안 사용할 힙 메모리 공간을 프로그래머가 할당하는 것을 이야기합니다. 프로그램 실행 시 크기가 결정되는 정적 메모리 할당과 대조됩니다.
[C/C++]
C/C++의 경우 표준 라이브러리(standard library - stdlib.h)에 포함된 malloc, calloc 함수를 통해 메모리를 할당하고, realloc 함수를 통해 재할당합니다.
세 함수는 다음과 같이 정의되어 있습니다.
void * malloc ( size_t size );
void * calloc ( size_t num, size_t size );
void * realloc ( void * ptr, size_t size );
malloc은 size 바이트만큼의 메모리를, calloc은 num * size 만큼의 메모리를 할당합니다.
할당한 메모리는 프로그램이 종료되기 이전에 반드시 free 함수를 통해 해제해주어야 합니다.
void free ( void * ptr );
size_t 자료형에 관한 포스팅
다만 C++에서는 malloc calloc뿐 아니라 언어 차원에서 제공되는 new, delete 키워드를 이용하여 동적 메모리를 할당할 수도 있습니다.
[예시]
int *ptr = new int[3];
delete ptr;
두 방법의 차이는 malloc 함수의 경우 (힙 용량이 모자라거나 등의 다양한 이유로 인해) 동적 메모리 할당에 실패하면 널 포인터(0)를 리턴해주는데, new 키워드의 경우 std::bad_alloc 익셉션을 리턴해줍니다. 즉, try-catch문과 플래그 변수를 통해 메모리 할당 실패 시점을 알 수 있습니다. malloc과 마찬가지로 널 포인터를 리턴받고 싶을 경우 nothrow 키워드를 이용하면 됩니다.
int *ptr = (nothrow)new int[-3];
int *ptr = (nothrow)new int[pow(100, 100)];
=> 크기가 음수이거나 매우 크거나 힙 영역이 모자라거나 등 다양한 케이스에서 할당 실패
또한, 생성 타입이 일반 자료형이 아닌 클래스의 경우, malloc은 공간만 할당해주지만 new는 공간 할당과 더불어 객체를 생성해줍니다. 즉, 아래의 경우 생성자를 호출합니다.
#include <iostream> class Data { public: Data() { std::cout << "생성자 호출\n"; } }; int main() { Data* ptr = new Data; delete ptr; return (0); }
[Java]
자바는 JVM이라는 가상머신 위에서 동작하기때문에 메모리의 구분이 약간 다릅니다. (기본 구조는 같으나 버츄얼머신이 구조를 다르게 구분하여 사용하는 경우)
자바는 기본적으로 Heap Memory와 Non-Heap Memory로 나뉘는데, Heap Memory에는 모든 자바 클래스의 인스턴스와 배열이 저장되고, Non-Heap Memory에는 그 밖의 데이터들이 저장됩니다. 이에 대해 자세히 정리한 외부 포스팅이 있으니 참고하시길 바랍니다.
자바에서는 C++과 마찬가지로 new 키워드를 통해 동적 메모리를 할당합니다. 상기하였듯 new 키워드를 통해 배열의 크기를 지정하고, 인스턴스를 생성하여 Heap Memory에 저장합니다.
JVM(Java Virtual Machine)의 가비지 컬렉터(GC, Garbage Collector)가 사용하지 않는 메모리는 자동적으로 Old로 분류하여 프로그램 종료시 해제해주므로 delete나 free 등은 사용하지 않습니다.
[Python]
파이썬은 기본적으로 모든 것이 객체입니다. (그렇다고 모든 자료가 Heap에 저장되는것은 아닙니다)
파이썬 메모리 관리자가 자동적으로 메모리를 관리해주기 때문에 동적 할당과 해제가 필요하지 않고 불가하지만, 제한적으로 동적 변수 할당을 해줄 수 있지만 C/C++의 동적 메모리 할당과는 다른 개념입니다.
다만 메모리 관리자가 Heap을 효율적, 효과적으로 사용하도록 프로그래머가 돕는 방법이 있습니다. 관련하여 자세히 다룬 포스팅이 있어 소개합니다.
그 밖의 언어는 제가 다뤄보지 않아서 잘 모르겠습니다. 다만 Kotlin의 경우 Java와 동일하게 JVM상에서 돌아가기 때문에 같은 매커니즘으로 동작할것이라 예측하고, C#의 경우 모든 것이 객체이기 때문에 Python과 유사하게 동작할 것이라 예상합니다.