목차
가상 메모리가 프로세스에게 자기만의 주소공간을 줬다. 그 공간 안에서도 데이터가 사는 동네가 나뉜다. 지역변수와 new로 만든 객체는 전혀 다른 곳에 산다.
지역변수와 new는 사는 곳이 다르다
코드 한 조각을 보자.
void handle() {
int count = 0; // 지역변수
Order o = new Order(); // 직접 할당한 객체
}count와 o는 같은 함수 안에 있지만 사는 곳이 다르다. count 같은 지역변수는 스택(stack), new로 얻은 객체는 힙(heap) 에 놓인다. 이 둘의 성격이 완전히 달라서, 프로그램의 여러 현상이 여기서 갈린다.
스택: 함수마다 쌓이는 접시
스택은 이름 그대로 접시를 쌓듯 동작한다. 함수를 호출하면 그 함수의 지역변수·인자를 담을 칸(스택 프레임)이 위에 하나 쌓이고, 함수가 끝나면 그 칸이 위에서 걷힌다.
main() 호출 → [main 프레임]
handle() 호출 → [main][handle 프레임] ← 쌓임
validate() 호출 → [main][handle][validate] ← 또 쌓임
validate() 끝 → [main][handle] ← 걷힘가장 최근에 부른 함수가 항상 맨 위에 있다. 함수가 다른 함수를 부르면 쌓이고, 돌아오면 걷힌다. 이 규칙적인 쌓기·걷기가 스택의 전부다.
스택은 자동으로 정리된다
여기서 스택의 가장 큰 장점이 나온다 - 정리가 공짜다. 함수가 끝나면 그 프레임이 통째로 걷히면서 지역변수도 함께 사라진다. 개발자가 “이 변수 지워라”를 할 필요가 없다.
대신 대가가 있다. 스택 변수는 함수가 끝나면 사라지므로, 함수 밖으로 값을 오래 들고 나갈 수 없다. 함수가 반환된 뒤에도 살아 있어야 하는 데이터는 스택에 둘 수 없다. 그런 건 힙으로 가야 한다.
스택이 넘치면: 스택 오버플로
스택은 크기가 정해져 있다(스레드마다 정해진 만큼). 함수가 함수를 부르며 프레임이 계속 쌓이기만 하면, 이 정해진 크기를 넘는다. 그게 스택 오버플로(stack overflow) 다.
가장 흔한 원인은 끝없는 재귀다. 자기가 자기를 부르는데 멈추는 조건이 없으면, 프레임이 무한히 쌓여 스택을 넘고 프로그램이 죽는다.
recurse() → recurse() → recurse() → ... → 💥 스택 넘침“재귀가 깊으면 위험하다”는 말의 밑바닥이 이것이다. 스택은 싸고 빠르지만 양이 한정돼 있어, 너무 깊이 쌓으면 넘친다.
힙: 직접 얻어 오래 두는 공간
힙은 다르다. 필요할 때 “이만큼 메모리 줘”라고 직접 요청해 얻고, 함수가 끝나도 안 사라진다. 그래서 함수 밖으로 오래 들고 다녀야 하는 것, 크기가 실행 중에 정해지는 것이 힙으로 간다.
힙은 함수 수명에 매이지 않아 자유롭지만, 그 자유의 대가로 누가 언제 정리하느냐는 문제가 생긴다.
힙은 누가 정리하나
스택은 자동으로 걷혔지만, 힙은 안 사라진다. 그럼 다 쓴 힙 메모리는 누가 치우나. 언어마다 답이 다르다.
- 직접 치운다 - C 같은 언어는 개발자가 “이제 이 메모리 반납”을 직접 부른다. 안 하면 메모리 누수, 이미 반납한 걸 또 쓰면 버그.
- GC가 치운다 - Java·Go·파이썬 등은 가비지 컬렉터가 “아무도 안 가리키는 힙 객체”를 찾아 자동으로 회수한다. 편하지만 그 회수 작업이 이따금 CPU를 쓴다.
어느 쪽이든 힙은 스택처럼 공짜로 걷히지 않는다 - 사람이 하든 GC가 하든, 정리라는 일이 따로 필요하다.
왜 둘로 나눴나
굳이 나눈 이유는 수명과 속도다.
- 스택은 빠르다 - 쌓고 걷는 게 포인터 하나 옮기는 일이라 즉시다. 대신 함수 수명에 매이고 양이 적다.
- 힙은 유연하다 - 오래 살고 크기도 자유롭다. 대신 얻고 정리하는 데 비용이 든다.
짧게 살고 빠르면 되는 것(지역변수)은 스택, 오래 살고 커야 하는 것(객체·컬렉션)은 힙. 성격이 반대라 공간을 갈라 각자 잘하는 걸 시킨다.
실무에서: 두 동네에서 생기는 두 사고
- 스택 오버플로 - 대개 무한 재귀거나 너무 깊은 호출이다. 재귀 깊이를 제한하거나 반복문으로 바꾼다. 스택을 키우는 건 임시방편이다.
- 메모리 누수 - 힙에 얻어놓고 안 놓아준 것. GC 언어에서도 “안 쓰는데 어딘가가 계속 가리키고 있으면” GC가 못 치워 샌다(예: 계속 쌓기만 하는 캐시·리스트). 무엇이 그 객체를 붙들고 있는지를 찾는 게 핵심이다.
- “힙이 부족하다” - 힙이 계속 자라 상한에 부딪히면 프로그램이 죽는다. 누수인지, 정말 그만큼 필요한지를 가려야 한다.
정리
- 지역변수는 스택,
new로 얻은 객체는 힙에 산다 - 같은 주소공간의 다른 동네다. - 스택은 함수마다 쌓였다 걷히며 자동 정리되지만 양이 정해져 있어, 너무 깊이 쌓으면 스택 오버플로.
- 힙은 함수가 끝나도 남아 오래 들고 다닐 수 있지만, 직접 또는 GC로 정리해야 하고 안 하면 누수.
- 나눈 이유는 수명과 속도 - 짧고 빠른 건 스택, 오래 살고 큰 건 힙.
다음 글은 이 스택을 각자 하나씩 갖는 실행 단위 - 스레드로 간다.