이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › Java › JVM
스택과 힙
메서드를 부를 때마다 메모리에서는 무슨 일이
메서드를 호출하면 그 스레드의 스택에 프레임이 하나 쌓이고, 반환하면 사라집니다. new로 만든 객체는 스택이 아니라 힙에 놓이고 프레임의 지역 변수가 그것을 가리킵니다.
실행으로 대조 · JDK 21에서 실행해 확인
이 실습에 나오는 개념
- 런타임 데이터 영역
- JVM이 프로그램을 실행하면서 사용하는 메모리 영역이다. 스레드마다 따로 갖는 영역으로 PC 레지스터, JVM 스택, 네이티브 메서드 스택이 있고, 모든 스레드가 공유하는 영역으로 힙과 메서드 영역이 있다. 어떤 데이터가 스레드 간에 공유되는지를 이해하는 기준이 된다.
- JVM 스택
- 스레드마다 하나씩 만들어지는 메모리 영역이다. 메서드가 호출될 때마다 그 메서드의 지역 변수와 중간 계산값을 담은 프레임이 쌓이고, 메서드가 끝나면 제거된다. 스레드별로 분리되어 있어 지역 변수는 다른 스레드와 공유되지 않는다. 재귀 호출이 너무 깊어지면 StackOverflowError가 발생한다.
- 힙(Heap) 메모리
- new로 만든 모든 객체와 배열이 저장되는 메모리 영역으로, 모든 스레드가 공유한다. 가비지 컬렉터가 관리하며 더 이상 쓰이지 않는 객체를 회수한다. 크기는 -Xms(초기), -Xmx(최대) 옵션으로 정하고, 공간이 부족하면 OutOfMemoryError가 발생한다.
- StackOverflowError와 OutOfMemoryError
- StackOverflowError는 메서드 호출이 너무 깊어져 스레드의 스택 공간을 넘었을 때 발생하며 주로 종료 조건이 잘못된 재귀가 원인이다. OutOfMemoryError는 힙 등의 메모리가 부족해 객체를 더 할당할 수 없을 때 발생하며 메모리 누수나 한 번에 너무 많은 데이터를 올리는 것이 흔한 원인이다.
이렇게 바꿔 보세요
- 처음 설정으로 — factorial(4). 프레임이 5개까지 쌓였다가 거꾸로 빠집니다.
- 종료 조건을 빼면 — 끝없이 자기를 부르다 스택이 넘칩니다.
- 종료 조건이 있어도 너무 깊으면 — factorial(10)은 프레임 11개가 필요합니다. 스택은 8개까지입니다.
- 스택을 키우면 — 같은 factorial(10)이 정상적으로 끝납니다.
- 호출마다 객체 생성 — 객체는 힙에 생기고, 프레임이 사라지면 가리키는 곳이 없어집니다.
처음 설정으로 실행하면
- 프로그램 시작. main 스레드의 스택에 main() 프레임이 놓입니다. 프레임에는 그 메서드의 지역 변수와 "어디로 돌아갈지"가 들어 있습니다.
- factorial(4) 호출. 새 프레임이 스택 맨 위에 쌓입니다. 지역 변수 n(값 4)은 이 프레임 안에만 있습니다.
- factorial(3) 호출. 새 프레임이 스택 맨 위에 쌓입니다. 지역 변수 n(값 3)은 이 프레임 안에만 있습니다.
- factorial(2) 호출. 새 프레임이 스택 맨 위에 쌓입니다. 지역 변수 n(값 2)은 이 프레임 안에만 있습니다.
- factorial(1) 호출. 새 프레임이 스택 맨 위에 쌓입니다. 지역 변수 n(값 1)은 이 프레임 안에만 있습니다.
- factorial(1) 호출이 끝났습니다. 반환값은 종료 조건에 걸려 1입니다. 프레임이 스택에서 사라지고, 그 안의 지역 변수도 함께 없어집니다.
- factorial(2) 호출이 끝났습니다. 반환값은 2 × 1 = 2입니다. 프레임이 스택에서 사라지고, 그 안의 지역 변수도 함께 없어집니다.
- factorial(3) 호출이 끝났습니다. 반환값은 3 × 2 = 6입니다. 프레임이 스택에서 사라지고, 그 안의 지역 변수도 함께 없어집니다.
- factorial(4) 호출이 끝났습니다. 반환값은 4 × 6 = 24입니다. 프레임이 스택에서 사라지고, 그 안의 지역 변수도 함께 없어집니다.
- main()이 결과를 출력합니다: 24. 스택은 가장 나중에 쌓인 프레임부터 먼저 빠졌습니다(후입선출).
실행 결과
- 프레임은 호출할 때 쌓이고 반환할 때 사라집니다. 가장 깊을 때 5개였습니다. 지역 변수는 프레임과 수명이 같아서 따로 정리할 필요가 없습니다.
코드
static long factorial(int n) {
if (n <= 1) return 1; // 종료 조건
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println(factorial(4));
}
이 모형의 전제
스택 크기를 "프레임 개수"로 단순화했습니다. 실제로는 바이트 단위의 크기(-Xss, 64비트 리눅스 기본 1MB 안팎)이고, 프레임마다 지역 변수 수에 따라 크기가 달라서 들어가는 프레임 수가 일정하지 않습니다. JIT 컴파일 여부에 따라서도 달라집니다. 스택이 스레드마다 따로 있고 힙은 공유된다는 점, 프레임이 호출 시 쌓이고 반환 시 사라진다는 점은 JVM 명세의 정의와 같습니다. 이 PC의 JDK 21에서 실제로 실행해 확인했습니다. 종료 조건이 없는 재귀는 깊이 약 1만 7천에서 StackOverflowError가 났습니다. 종료 조건이 있어도 깊이 20만은 기본 스택에서 StackOverflowError가 났고, 스택을 64MB로 키우자(-Xss64m) 정상 종료했습니다. 한 스레드의 스택이 넘친 뒤에도 다른 스레드는 계속 실행됐습니다.
앞 실습: 블로킹·논블로킹·가상 스레드 · 다음 실습: GC와 메모리 누수