자바(Java) 백엔드 개발자로서 애플리케이션을 안정적으로 운영하고 성능을 최적화하기 위해서는 자바 가상 머신(JVM, Java Virtual Machine)의 메모리 구조를 명확히 이해해야 합니다. 우리가 작성한 코드가 실행될 때 메모리에서 어떤 일이 일어나는지 모른다면, 실무에서 자주 발생하는 OutOfMemoryError나 성능 저하 문제를 해결할 수 없습니다.
본 글에서는 JVM의 핵심 메모리 영역인 런타임 데이터 영역(Runtime Data Area)의 구조를 살펴보고, 그중에서도 가장 중요한 JVM Stack 영역과 Heap 영역의 차이점과 상호작용 방식을 상세히 알아보겠습니다.
1. JVM 런타임 데이터 영역(Runtime Data Area)이란?
자바 프로그램이 실행되면 JVM은 운영체제(OS)로부터 해당 프로그램이 사용할 메모리를 할당받습니다. 이 할당받은 메모리 공간을 런타임 데이터 영역(Runtime Data Area)이라고 부릅니다.
런타임 데이터 영역은 크게 5가지 공간(메서드 영역, 힙 영역, 스택 영역, PC 레지스터, 네이티브 메서드 스택)으로 나뉘지만, 개발자가 소스 코드를 작성하고 데이터를 다룰 때 가장 밀접하게 연관되는 곳은 바로 스택(Stack) 영역과 힙(Heap) 영역입니다.
- 스택(Stack) 영역: 스레드(Thread)마다 독립적으로 할당되며, 메서드 실행과 관련된 지역 변수와 매개 변수가 저장됩니다.
- 힙(Heap) 영역: 모든 스레드가 공유하며,
new연산자를 통해 생성된 객체와 배열이 저장됩니다.
이 두 영역의 특징과 차이를 명확히 아는 것이 자바 메모리 관리의 첫걸음입니다.
2. JVM Stack 영역의 특징과 작동 원리
2-1. 스레드 독립적인 메모리 공간
JVM Stack 영역은 프로그램이 실행될 때 각 스레드(Thread)마다 하나씩 생성됩니다. 스레드가 시작될 때 할당되고 스레드가 종료되면 함께 소멸하므로, 다른 스레드에서는 접근할 수 없는 스레드 안전(Thread-Safe)한 공간입니다.
2-2. 스택 프레임(Stack Frame) 구조
스택 영역은 이름 그대로 LIFO(Last In, First Out / 후입선출) 구조로 동작합니다. 메서드가 호출될 때마다 스택 영역에 ‘스택 프레임(Stack Frame)’이라는 블록이 쌓이고, 메서드 실행이 끝나면 해당 스택 프레임이 팝(Pop)되어 완전히 사라집니다.
스택 프레임 내부에는 다음과 같은 정보들이 저장됩니다.
- 지역 변수 배열(Local Variables Array): 메서드 안에서 선언된 기본 타입(int, double 등) 변수와 참조 타입(객체의 주소) 변수가 저장됩니다.
- 피연산자 스택(Operand Stack): 메서드 내에서 연산을 수행할 때 임시로 데이터를 저장하는 공간입니다.
- 프레임 데이터(Frame Data): 상수 풀(Constant Pool) 참조나 예외 처리 정보 등이 담깁니다.
💡 주의해야 할 점 (StackOverflowError)
메서드가 무한 루프를 돌며 재귀 호출되거나, 너무 많은 스택 프레임이 쌓여 할당된 스택 메모리 크기를 초과하면
java.lang.StackOverflowError가 발생합니다.
3. JVM Heap 영역의 특징과 구조
3-1. 모든 스레드가 공유하는 공간
Heap 영역은 JVM이 시작될 때 생성되며, 애플리케이션 내의 모든 스레드가 공유합니다. new 키워드를 사용해 동적으로 생성된 객체(인스턴스)와 배열이 이 공간에 저장됩니다.
3-2. 가비지 컬렉션(Garbage Collection)의 대상
스택 영역의 데이터는 메서드 종료와 함께 자동으로 사라지지만, 힙 영역의 객체는 메서드가 끝나도 사라지지 않습니다. 더 이상 스택 영역에서 참조하지 않는(쓰이지 않는) 힙 영역의 ‘쓰레기 객체’들은 가비지 컬렉터(GC)가 수거해 갈 때까지 메모리에 남아있게 됩니다.
3-3. 힙(Heap) 영역의 세부 구성
효율적인 가비지 컬렉션을 위해 힙 영역은 크게 두 가지 영역으로 나뉩니다.
- Young Generation: 새롭게 생성된 객체들이 위치하는 곳입니다. 대부분의 객체는 금방 접근 불가능한 상태가 되기 때문에, 이곳에서 수많은 객체가 생겼다 사라집니다. 여기서 일어나는 GC를 Minor GC라고 합니다.
- Old Generation: Young 영역에서 살아남은 객체들이 이동하는 곳입니다. 보통 Young 영역보다 크게 할당되며, 이곳의 메모리가 부족해지면 Major GC(또는 Full GC)가 발생합니다.
💡 주의해야 할 점 (OutOfMemoryError)
사용이 끝난 객체의 참조를 끊지 않고 계속 쥐고 있으면 GC가 이를 수거하지 못해 메모리가 누수(Memory Leak)되며, 결국 힙 공간이 부족해져
java.lang.OutOfMemoryError: Java heap space가 발생하게 됩니다.
4. Stack과 Heap은 어떻게 상호작용하는가?
이론만으로는 이해하기 어려우니, 간단한 자바 코드 예시를 통해 두 영역이 어떻게 상호작용하는지 살펴보겠습니다.
public class MemoryExample {
public static void main(String[] args) {
int age = 25;
String name = new String("James");
}
}
위 main 메서드가 실행될 때 메모리에서는 다음과 같은 변화가 일어납니다.
main메서드가 호출되면서 JVM Stack 영역에 main 스택 프레임이 생성됩니다.int age = 25;문장이 실행되면, 기본 타입 변수인age와 그 값인25는 Stack 영역(main 프레임 내부)에 직접 저장됩니다.String name = new String("James");문장이 실행되면 두 가지 단계로 나뉩니다.new String("James")에 의해 실제 문자열 객체는 Heap 영역에 생성됩니다.- 참조 변수인
String name은 Stack 영역에 생성되며, 변수의 값으로는 Heap 영역에 있는 객체의 ‘메모리 주소(참조값)’가 저장됩니다.
즉, “기본 타입의 값과 참조 타입의 ‘주소’는 스택에 저장되고, 실제 객체의 ‘본체’는 힙에 저장된다”고 이해하시면 됩니다. 메서드가 종료되면 age와 name 변수가 있는 스택 프레임은 날아가지만, 힙 영역의 James 객체는 남아서 나중에 GC에 의해 정리됩니다.

5. 핵심 요약: 한눈에 비교하는 Stack vs Heap
마지막으로 두 영역의 핵심 차이점을 표로 정리해 보겠습니다.
| 구분 | JVM Stack 영역 | JVM Heap 영역 |
| 관리 주체 | 스레드별로 자동 관리 (LIFO 구조) | 가비지 컬렉터(GC)가 관리 |
| 공유 여부 | 스레드 간 공유 불가능 (Thread-Safe) | 모든 스레드가 공유 (동기화 이슈 발생 가능) |
| 저장 데이터 | 기본 타입 변수, 참조 변수의 주소값 | new로 생성된 객체, 인스턴스, 배열 |
| 생성 시점 | 스레드가 시작될 때 / 메서드가 호출될 때 | JVM이 시작될 때 (프로그램 구동 시) |
| 관련 에러 | StackOverflowError | OutOfMemoryError |
6. 결론
자바 백엔드 개발자에게 JVM 메모리 구조를 이해하는 것은 선택이 아닌 필수입니다. 내가 만든 객체가 힙 메모리 어디에 머무는지, 내가 호출한 메서드가 스택 메모리를 어떻게 소모하는지 인지하고 코드를 작성하면 오버헤드를 대폭 줄일 수 있습니다.
다음 글에서는 힙 영역을 청소하여 메모리를 확보하는 가비지 컬렉션(Garbage Collection)의 구체적인 동작 원리에 대해 자세히 알아보겠습니다.