J-S-07 자바 개발자라면 꼭 알아야 할 JVM 메모리 구조와 GC
글 정보
- 카테고리
- Programming/Java/Starter
- 태그
- JavaLevel2
1. 런타임 데이터 영역의 전체 구조
JVM이 운영체제로부터 할당받는 메모리 영역인 '런타임 데이터 영역'은 크게 두 가지로 나눌 수 있습니다. 모든 스레드가 공유하는 영역과, 각 스레드가 개별적으로 가지는 영역입니다.
공유 영역
- Method Area: 클래스 정보, 상수, 정적 변수 등이 저장됩니다.
- Heap Area: 인스턴스와 배열이 생성되는 가장 큰 메모리 공간입니다.
스레드별 고유 영역 (Thread Private)
- Stack Area: 메서드 호출 시 생성되는 스택 프레임이 저장됩니다.
- PC Register: 현재 실행 중인 명령어 주소를 가리킵니다.
- Native Method Stack: 자바 외의 언어(C/C++ 등)로 작성된 코드를 위한 공간입니다.
2. 스레드별 고유 영역 상세 분석
스레드가 생성될 때마다 만들어지는 이 영역들은 스레드의 실행 흐름을 제어하는 중요한 역할을 합니다.
PC Register (Program Counter)
JVM은 기본적으로 스택 기반의 가상 머신이지만, 내부적으로는 레지스터 머신의 개념을 차용하고 있습니다.
그중 핵심이 바로 PC Register입니다.
이곳에는 현재 스레드가 바이트 코드 수준에서 무엇을 하고 있는지, 그리고 다음에 어떤 명령을 수행해야 하는지에 대한 정보가 담겨 있습니다.
Native Method Stack
자바가 아닌 C나 C++로 작성된 네이티브 코드를 실행하기 위한 메모리 영역입니다.
PC의 네이티브 영역과 연결되어 작동합니다.
일반적인 웹 애플리케이션 개발 단계에서는 깊게 다루지 않아도 되는 부분이니 가볍게 넘어가셔도 좋습니다.
Stack Area (스택 영역)
우리가 흔히 "스택 메모리"라고 부르는 곳입니다.
메서드가 호출될 때마다 프레임이 쌓이고 메서드가 종료되면 사라집니다.
이 영역은 크기가 한정적이기 때문에, 재귀 호출이 너무 깊어지면 StackOverflowError가 발생할 수 있습니다.
3. Method Area와 Metaspace
Method Area는 런타임 데이터 영역에서 가장 주의 깊게 살펴봐야 할 곳 중 하나입니다.
JVM이 클래스 파일을 로드할 때 읽어 들인 타입 정보, 상수, 정적 변수(static) 등이 이곳에 저장됩니다.
또한 JIT(Just-In-Time) 컴파일러가 번역한 기계어 코드를 캐싱하는 공간이기도 합니다.
Java 8 이후의 변화: Metaspace 과거에는 PermGen이라는 이름으로 힙 영역의 일부처럼 관리되었지만, Java 8부터는 Metaspace라는 이름으로 변경되었습니다.
- 특징: JVM 힙이 아닌 네이티브 메모리에서 관리됩니다.
- 장점: 크기가 동적으로 조정되므로 메모리 관리 측면에서 유연성이 늘어났습니다.
Runtime Constant Pool (런타임 상수 풀) Method Area 내부에는 클래스 버전, 필드, 메서드, 인터페이스 등의 정보와 각종 리터럴이 저장되는 상수 풀이 존재합니다.
이는 동적으로 운영되므로 런타임에 새로운 상수가 추가될 수도 있습니다.
Spring Framework 등을 사용할 때 이 영역이 활발하게 사용되므로 생각보다 많은 공간을 차지할 수 있습니다.
4. VMS (가상 메모리 공간)와 메모리 제한
JVM은 운영체제 입장에서 하나의 프로세스이며, 자신만의 가상 메모리 공간(VMS)을 가집니다.
만약 32bit 애플리케이션 환경이라면 최대 메모리는 $2^{32}$바이트, 즉 4GB로 제한됩니다.
여기서 OS가 사용하는 2GB를 제외하고 프로세스 자체의 오버헤드를 빼면, 실제 자바 애플리케이션이 쓸 수 있는 메모리는 약 1.7~1.8GB 수준입니다.
클라이언트 PC라면 큰 문제가 아닐 수 있지만, 대용량 트래픽을 처리해야 하는 서버 환경이라면 이야기가 달라집니다.
(따라서 최신 서버 환경은 대부분 64bit JVM을 사용하여 이러한 메모리 제약을 극복하고 있습니다.)
5. JVM 스택의 내부 구조 (Deep Dive)
JVM의 스택은 단순히 데이터만 쌓는 곳이 아니라, C/C++의 스택보다 훨씬 복잡하고 정교한 구조를 가집니다. 스택 프레임 내부를 자세히 들여다보겠습니다.
지역변수 테이블 (Local Variable Table)
흔히 스택이라 부르는 공간의 실체입니다. 이곳은 '슬롯(Slot)'이라는 단위로 관리됩니다.
- 기본형 변수(int, boolean 등)는 1개의 슬롯을 차지합니다. (long이나 double은 2개의 슬롯을 사용하기도 합니다.)
- 슬롯에는 인덱스 번호가 매겨져 있으며,
this(0번), 매개변수(1번), 지역변수(n번) 순으로 저장됩니다.
피연산자 스택 (Operand Stack)
계산을 위한 임시 저장소입니다.
JVM 스택의 크기
재미있는 점은 Java 스택의 크기가 단순 메모리 용량이 아니라 슬롯의 개수로 결정된다는 것입니다.
물론 -Xss 옵션으로 메모리 크기를 지정하지만, 내부적으로는 처리 가능한 스택 프레임의 깊이와 슬롯 수에 의해 StackOverflowError가 결정됩니다.
주의: 스택은 스레드마다 완전히 분리되어 있다는 점을 꼭 기억해 주세요!
6. 힙(Heap) 영역과 가비지 컬렉션(GC)
JVM 메모리의 대부분은 Heap Area가 차지합니다.
이곳은 사용자가 생성한 객체의 인스턴스와 배열이 저장되는 곳이며, GC(Garbage Collector)의 주요 관리 대상입니다.
세대별 컬렉션 이론 (Generational Collection Theory)
효율적인 메모리 관리를 위해 힙 영역은 객체의 생명 주기에 따라 나뉩니다.
- Young Generation (Eden, Survivor): 갓 생성된 객체들이 위치합니다.
- Old Generation: 오랫동안 살아남은 객체들이 이동해 옵니다.
- Metaspace: (앞서 언급한 대로 Java 8부터는 네이티브 메모리로 이동했습니다.)
가비지 컬렉터 (GC)의 동작
GC는 힙 영역에서 더 이상 참조되지 않는 객체를 찾아 제거합니다. GC가 동작하는 방식은 크게 세 가지 핵심 요소로 나뉩니다.
- 회수 대상 판단: 어떤 객체가 쓰레기인가?
- 회수 시점: 언제 청소할 것인가?
- 회수 방법: 어떻게 메모리를 비우고 정리할 것인가?
GC의 종류와 영향
- Minor GC: Young 영역을 청소하며, 보통 1초 이내로 매우 빠르게 끝납니다.
- Major (Full) GC: Old 영역까지 포함하여 청소합니다. 시간이 수 초 이상 걸릴 수 있으며, 이때 Stop-the-world(애플리케이션이 일시 정지하는 현상)가 발생합니다.
Full GC가 너무 빈번하게 발생하면 서버가 멈춘 것처럼 보여 장애로 이어질 수 있으므로, 적절한 튜닝과 메모리 관리가 필수적입니다.