
👩🏻💻 C 언어의 메모리 누수의 발생과 해결방법
이번엔 C언어에서의 메모리 누수가 일어나는 원인에 대해, 그 중에서 포인터에서 누수가 많이 일어나는 이유에 대해 알아보려고 한다. 그리고 이 때 어떤 방식으로 메모리 누수를 해결하고 이를 바탕으로 내가 많이 사용하고 있는 JavaScript 에서는 어떻게 메모리 누수를 관리하면 좋을지에 대해서 공부를 해보고자 한다!
🖐🏻 C 언어에서의 메모리 누수를 들어가기 전에,
📝 C언어의 메모리 구조

📝 메모리 구조 영역 구분
code(text), data, stack 영역
- 컴파일러가 컴파일 할 때 그 크기를 계산해 필요한 메모리 공간을 결정
heap
- 개발자에 의해 프로그램 동작 시 결정됨
📝 SMA & DMA
Static Memory Allocation
정적 메모리
메모리의 data 영역, stack 영역을 사용함
data 영역 : 프로그램 시작과 동시에 할당된 영역이 잡히고 끝나면 OS 에 반환
stack 영역 : 함수 시작과 동시에 할당된 영역이 잡히고 끝나면 OS 에 반환
Dynamic Memory Allocation
동적 메모리
메모리의 heap 영역을 사용
- heap 영역 : stack 에서 pointer 변수를 할당하고, 그 pointer가 가리키는 heap 영역의 임의의 공간부터 원하는 크기만큼 할당해 사용
📝 메모리 구조의 각 영역

stack
프로그램이 자동으로 사용하는 임시 메모리 영역
지역변수, 매개변수, 리턴 값 등 잠시 사용되었다가 사라지는 데이터를 저장하는 영역
함수 호출 시 생성되고 함수가 끝나면 시스템에 반환됨
스택 사이즈는 각 프로세스마다 할당되지만 프로세스가 메모리에 로드될 때 사이즈가 고정되어 있어 런타임 시 스택 사이즈를 바꿀 수는 없음
명령 실행 시 자동 증가/감소 하므로 보통 메모리의 마지막 번지를 지정함
heap
필요에 의해 동적으로 메모리를 할당하고자 할 때 위치하는 메모리 영역
할당해야 할 메모리의 크기를 프로그램이 실행되는 동안 결정해야하는 경우(런 타임 때) 유용하게 사용되는 공간
동적 데이터 영역이라고 부름
메모리 주소 값에 의해서만 참조되고 사용되는 영역
C 에서는
malloc(), C++ 은new()함수를 사용
static data 영역 (initialized data)
전역변수(global), 정적변수(static), 배열(array), 구조체(structure) 등이 저장됨
초기화된 데이터는 data 영역에 저장
초기화되지 않은 데이터는 BSS(blocked stated symbol) 영역에 저장
프로그램이 실행될 때 생성되고 프로그램이 종료되면 시스템에 반환됨
함수 내부에 선언된 static 변수는 프로그램이 실행될 때 공간만 할당되고, 그 함수가 실행될 때 초기화됨
BSS(blocked stated symbol) 영역
- 초기화되지 않은 데이터가 저장되는 영역
text(code) 영역
코드 자체를 구성하는 메모리 영역
- Hex 파일이나 BIN 파일 메모리
프로그램 명령이 위치하는 곳으로 기계어로 제어되는 메모리 영역
📝 각 영역에 저장되는 자료들
stack (스택)
해당 영역에 메모리를 블록처럼 쌓아놓는 방식 (자료 구조에서의 스택 이해해보기)
블록이 차곡차곡 쌓여있으므로 중간 블록을 빼는 것은 불가
- 마지막 블록부터 차곡차곡 빼내야 함
지역 변수들과 매개변수 및 리턴주소가 저장됨
스택에 저장된 값들은 함수 호출이 종료되면 해제됨
[ stack 예제 1 ]

void func2(int arg2_1, int arg2_2) {
int local_var_2 = 3;
printf("local_var_2=&d, arg2_1=%d, arg2_2=%d\\n",
local_var_2, arg_2, arg2_2);
}
void func1(int arg1_1, int arg1_2) {
int local_var_1_1 = 1;
int local_var_1_2 = 2;
printf("local_var_1_1=&d local_var_1_2=%d arg1_1=%d arg1_2=%d\\n",
local_var_1_1, local_var_1_2, arg1_1, arg1_2);
func2(30, 40);
}
int main() {
int main_local = 0;
printf("main_local=%d\\n", main_local);
func1(10, 20);
return 0;
}
main_local => arg1_1 => arg1_2 => main 의 리턴 주소값?(오ㅐ..?) =>
[ stack 예제 2 ]

#include <stdio.h>
void fct1(int);
void fct2(int);
int a = 10;
int b = 20;
int main() {
int i = 100;
fct1(i);
fct2(i);
return 0;
}
void fct1(int c) {
int d = 30;
}
void fct2(int e) {
int f = 40;
}
힙 (heap)
‘ 더미 ‘ 로 직역될 수 있음
스택이 함수 호출에 따라 메모리가 차곡차곡 쌓이는 모양이라면 힙은 메모리가 힙 영역에 아무렇게나 할당이 되어있음
힙의 대표적인 세 가지 특징
힙에 저장된 데이터는 함수 호출이 종료되어도 해제되지 않아서, 개발자가 명시적으로 해제하거나 프로그램이 종료되어야 해제됨
프로그램은 메모리 주소에 따라서 힙 데이터에 접근
힙 공간은 크기의 제약이 없어서 메모리가 충분하다면 힙 영역은 필요한 만큼 확장될 수 있음
힙의 특징들로 인해 발생되는 문제
사용이 종료된 데이터가 해제되지 않으면 프로그램이 종료될 때까지 메모리 공간을 차지함
프로그램은 힙에 데이터가 해제되었어도 메모리 주소에 따라 접근이 가능
힙 영역은 크기의 제한은 없지만 가용 메모리가 가득찰 경우 커널의 OOM Killer 에 의해 프로그램이 종료될 수 있음
힙에 메모리를 할당할 때는
malloc,calloc,realloc함수를 사용하고, 해제는free함수를 사용함
[ heap 예제 1 ]

#include <stdio.h>
#include <stdlib.h>
struct example {
int a;
int b;
char c;
};
int main() {
struct example *ex;
ex = malloc(sizeof(*ex));
ex->a = 1;
ex->b = 2;
ex->c = 'c';
printf("a=%d b=%d c=%c\\n", ex->a, ex->b, ex->c);
free(ex);
return 0;
}
🤔 C 언어에서 쉽게 겪을 수 있는 메모리 문제들의 유형
💦 메모리 릭 Memory Leak
프로그램이 불필요한 메모리를 계속 점유하는 현상
일반적으로 프로그램 가동 시간 혹은 사용량에 비례해 프로그램의 메모리 사용량이 점점 늘어나는 것으로 확인 가능

위와 같이 메모리 누수가 일어난 그래프를 확인해보면,
파란색 그래프 (SVG patch off)가 메모리 릭이 발생한 모습
- 사용 시간에 따라 점진적으로 메모리 사용량이 증가함
메모리 릭이 발생하지 않은 노란색 그래프(SVG patch on)
- 오르락 내리락하며 일정한 메모리 사용량을 보임
🧐 메모리 릭이 발생하는 이유?
힙 영역에 할당된 메모리를 해제하지 않아서 발생
C언어에서는 힙 영역에 동적 메모리를 할당함
힙 영역에 할당된 메모리는 함수 호출이 종료되어도 해제되지 않음
대신
free()등을 이용해 사용이 완료된 메모리를 명시적으로 해제해야 함메모리를 해제하지 않으면 메모리 릭이 발생함
😶🌫️ 댕글링 포인터 Dangiling Pointer
- 댕글링 포인터란 이미 해제된 메모리를 가르키는 포인터를 뜻함

동적 할당된 메모리를 가리키는 포인터가 있다고 가정할 때,
포인터는 메모리가 동적할당된 공간(힙)에 정상적으로 있다고 알고 있음
만약 여기서 동적 할당된 메모리가 해제되면 해당하는 공간을 가리키던 포인터에 있는 주소 공간은 아무런 의미가 없게 됨
이 때, 포인터에 저장된 주소 공간에서 데이터를 가져오거나 어떤 동작을 수행하면 문제가 될 수 있음
경우에 따라서는 당장은 쓰레기 값이 넘어올 뿐 정상동작하는 것처럼 보이다가 예상치 못한 경우에 크래쉬를 일으킬 수도 있음
#include <stdio.h>
#include <stdlib.h>
struct example {
int a;
int b;
char c;
};
int main() {
struct example *ex;
struct example *cloned;
ex = malloc(sizeof(*ex));
cloned = ex;
ex->a = 1;
ex->b = 2;
ex->c = 'c';
printf("[origin] a=%d b=%d c=%c\\n", ex->a, ex->b, ex->c);
printf("[cloned] a=%d b=%d c=%c\\n",
cloned->a, cloned->b, cloned->c);
free(ex);
ex = NULL;
/*
* 아래 라인은 Segfault를 일으킨다.
*/
// printf("[origin] a=%d b=%d c=%c\\n", ex->a, ex->b, ex->c);
/*
* 아래 라인에서 dangling pointer로 인한 이상한 값이 출력되었다.
*/
printf("[dangling] a=%d b=%d c=%x\\n",
cloned->a, cloned->b, cloned->c);
return 0;
}
- 위 코드를 실행하게 되면 동작에는 아무 문제가 없지만,
free()가 이뤄진 후에 출력되는 정보에는 쓰레기 값이 들어있음이 확인 가능함
[origin] a=1 b=2 c=c
[cloned] a=1 b=2 c=c
[dangling] a=1633718123 b=5 c=10
💥 버퍼 오버플로우 Buffer Overflow
- 배열 혹은 동적으로 할당된 메모리 영역을 벗어나서 쓰기 작업을 수행하는 것
🚧 널포인터 익셉션 Null Pointer Exception
Null 로 초기화한 메모리 영역에 접근하면 발생하는 에러
- 여담으로 이건 같이 최종 프로젝트 했던 스프링 백엔드 팀원이 매일 회의 때 이야기하던 단어였는데, 그 때는 그냥 null 이 들어가면 안되는군.. 하고 치웠던 개념이었는데 이제야 조금 이해가 감ㅋㅋ
👩🏻💻 그래서.. JS 에서 메모리 누수를 어떻게 관리할거니?
이것만 보면 자바스크립트에서의 메모리 누수를 줄일 방법에 대한 조금의 답을 찾을 수 있을까 싶었는데 아직은 잘 모르겠다.. C언어에서는 어쨌든 메모리를 해제시키는 함수가 있지만 자바스크립트는 그럼 함수가 있지 않고 가비지 컬렉터에 의해 정리가 되는 것으로 언뜻 알고있는 편이라.. 자바스크립트를 이용해서 개발을 할 때 개발자가 어떤 부분을 더 신경을 써줘야할 것인가에 대한 답은 아마도 자바스크립트를 더 공부해야 찾을 수 있을 것 같다.
여기에 자바스크립트까지 파고들면 포스트가 너무 길어질 것 같아서 2편에서 따로 정리를 해봐야겠다 ㅠ_ㅠ 그래도 조금은 힌트를 얻은 것 같은..! 더이상 사용하지 않는 데이터에 대해 클린업을 해준다는 부분과 포인터가 어떠한 부분을 참조하고 있다는 부분을 가져가서 공부를 해봐야겠다!
![[월간 이트루] 3월의 회고.](https://cdn.hashnode.com/res/hashnode/image/upload/v1711895194426/f6a22b40-ffd3-4b11-a25b-e1b040b11853.png)
![[월간 이트루] 2월의 회고.](https://cdn.hashnode.com/res/hashnode/image/upload/v1709129901980/5a2af3c5-8f6e-4f07-9c4f-1b43bef27ae9.png)


![[월간 이트루] 1월의 회고.](https://cdn.hashnode.com/res/hashnode/image/upload/v1706708547437/51af577d-21f9-4256-8939-95c577a58abc.jpeg)