들어가며
Java 애플리케이션이 실행되면 JVM(Java Virtual Machine) 이 메모리를 어떻게 사용하느냐는 성능과 안정성에 큰 영향을 줍니다.
특히 Heap, Stack, Metaspace는 각각의 역할이 명확하며, 개발자가 메모리 구조를 이해하고 있어야 메모리 누수, GC 문제 등을 예방할 수 있습니다.
이 글에서는 JVM 메모리 구조를 실무 중심으로 구조별 역할, 메모리 할당 위치, GC 관점에서의 차이점까지 상세하게 정리합니다.
1. JVM 메모리 영역 개요
┌────────────────────┐
│ 메서드 영역 │ → Metaspace (Java 8+)
├────────────────────┤
│ 힙 영역 (Heap) │ ← 객체 저장소, GC 대상
├────────────────────┤
│ 스택 영역 (Stack) │ ← 메서드 호출, 지역 변수
├────────────────────┤
│ 네이티브 메모리 영역│ ← JNI, 클래스 로더
└────────────────────┘
메모리 구조는 자바 버전에 따라 변동이 있습니다. (PermGen → Metaspace)
2. Heap – 객체가 저장되는 공간
✅ 특징
- new 키워드로 생성한 객체는 모두 Heap에 저장
- GC(Garbage Collector)의 주요 대상
- 크기가 크고 애플리케이션 전반에 공유됨 (힙에 저장된 객체는 다른 메서드, 스레드에서도 참조 가능)
String name = new String("John"); // name → Stack, 객체 → Heap
✅ 구조 세분화
영역 | 설명 |
Young Generation | 새로 생성된 객체들이 위치 (Eden + Survivor) |
Old Generation | 장기간 생존한 객체들이 이동됨 (Tenured) |
GC는 주로 Young 영역에서 자주 발생하고, Old 영역은 Full GC 시 대상이 됩니다.
3. Stack – 메서드 호출과 지역 변수 저장소
✅ 특징
- 각 스레드마다 독립된 Stack 공간을 가짐→ 즉, 여러 스레드가 동시에 실행되어도 자신의 메서드 호출 정보는 자신만의 스택에 저장됨
- 메서드 호출 시마다 스택 프레임이 생성됨→ 지역 변수, 매개변수, return 주소 등이 저장됨
- 메서드 종료 시 자동 제거 (GC 대상 아님)
💡 **프로그램이 시작되면 기본적으로 하나의 메인 스레드(main thread)**가 생성되고, 그에 대한 Stack도 함께 할당됩니다.
이후 개발자가 Thread를 추가로 생성하면 각각의 스레드는 자신만의 Stack 공간을 가집니다.
public void greet() {
String msg = "Hello"; // msg 참조 변수는 Stack, "Hello" 객체는 Heap
int age = 20; // 기본형 age 값은 Stack에 직접 저장
}
스택은 속도는 빠르지만 크기가 제한적이라 StackOverflowError가 발생할 수 있음
✅ 기본형 vs 참조형 저장 위치 요약
타입 구분 | Stack 저장 | Heap 저장 |
기본형(int, boolean 등) | ✅ 실제 값 저장 | ❌ 없음 |
참조형(String, 객체 등) | ✅ 참조 주소 저장 | ✅ 객체 본체 저장 |
변수명은 컴파일 타임까지만 존재하고, 런타임에는 사라집니다.
즉:
- int a = 10;에서 a는 개발자가 코드를 이해하기 위한 이름
- 컴파일되면 JVM 내부에서는 메서드의 스택 프레임 안에 슬롯(slot) 번호로 치환됩니다
예를 들어:
int a = 10;
int b = 20;
→ 실제로는 스택 안에 0번, 1번 슬롯에 저장될 뿐이고 "a"나 "b"라는 이름은 JVM 런타임엔 존재하지 않음
📌 단, 디버깅 시 변수명을 볼 수 있는 이유는 .class에 심볼 테이블이 포함되었기 때문입니다 (옵션으로 생략 가능)
- 자바 바이트코드 레벨에서는 지역 변수들이 번호로 된 슬롯(slot)에 저장됩니다.
- 슬롯(slot): 변수명이 아니라 바이트코드에서 JVM이 관리하는 지역 변수의 순번 공간이라는 개념
- 이 슬롯은 실제 물리적 메모리 주소는 아니고, JVM이 관리하는 논리적 번호 공간입니다.
- 예:
int a = 10; // → 슬롯 0번
String s = ...; // → 슬롯 1번 (참조 주소)
- 원시타입은 값 자체를, 참조타입은 주소를 슬롯에 저장한다
- 즉, 변수명이 아닌 순번으로 변수를 관리하며, 컴파일 이후에는 a, s 같은 이름은 없어지고 슬롯 번호만 남습니다.
4. Metaspace – 클래스 메타 정보 저장소 (Java 8+)
✅ PermGen → Metaspace 변화
- Java 7 이하: 메타 정보를 PermGen 영역에 저장 (크기 제한 있음)
- Java 8 이후: OS의 네이티브 메모리를 사용하는 Metaspace로 대체
✅ Metaspace에 저장되는 것
- 클래스 이름, 메서드/필드 정보, 어노테이션 등 클래스 로딩 관련 메타데이터
✅ 장점
- PermGen보다 유연한 크기 조정 (OutOfMemoryError 감소)
- XX:MaxMetaspaceSize로 최대 크기 지정 가능
네, 맞습니다. Metaspace는 자바(JVM)에서만 쓰는 메모리 영역 개념이에요.
- C/C++ 같은 언어는 클래스 자체가 없으니 이런 개념도 없음
- 자바에서는 클래스를 동적으로 로딩/해제할 수 있기 때문에, JVM은 이 클래스 메타데이터들을 따로 관리하는 공간이 필요해요 → 그것이 Metaspace입니다.
📌 저장되는 것:
- 클래스 이름, 상속 관계, 메서드 목록, 필드 타입, 어노테이션 등 "클래스 자체에 대한 정보"
5. 예제 기반 메모리 저장 구조 시각화
public class Main {
public static void main(String[] args) {
int a = 10; // Stack (기본형)
String s = new String("Hi"); // Stack(s), Heap("Hi")
User u = new User("Tom"); // Stack(u), Heap(User 객체)
}
}
변수명 | 타입 | Stack 저장 내용 | Heap 저장 내용 |
a | 기본형 | 값 10 | - |
s | 참조형 | 참조 주소 | "Hi" 문자열 객체 |
u | 참조형 | 참조 주소 | new User("Tom") 객체 |
6. 기타 메모리 영역
🔸 코드 캐시(Code Cache)
- JIT 컴파일된 네이티브 코드 저장 영역
🔸 Direct Memory
- NIO(ByteBuffer.allocateDirect 등)에서 사용됨
🔸 Thread Stack
- 각 스레드별로 Stack, PC Register 등 포함됨
6. GC 관점에서의 정리
메모리 영역 | GC 대상 여부 | 사용 예 | 주의 사항 |
Heap | ✅ 예 | new 객체, 배열 | GC 성능에 영향 큼 |
Stack | ❌ 아니오 | 지역 변수 | 너무 깊은 호출 시 StackOverflowError |
Metaspace | ❌ 아니오 | 클래스 정보 | 클래스 로더 누수 시 OOM 가능 |
스택 구조는 메서드를 호출할 때마다 스택 프레임이 위에 쌓이는데요,
void a() { b(); }
void b() { c(); }
void c() { d(); }
...
→ 이런 식으로 너무 많이 메서드를 호출하면, 결국 스택 공간이 **한계(깊이)**에 도달하고 StackOverflowError가 발생합니다.
더 극단적인 예:
void recur() {
recur(); // 자기 자신을 계속 호출
}
→ 스택이 무한히 쌓이다가 에러 납니다.
JVM은 클래스 로더가 로드한 클래스를 클래스 로더가 GC 대상이 될 때까지 해제하지 않음
누수가 발생하는 경우:
- WebApp을 재배포하면 새로운 클래스 로더가 생성되는데,
- 이전 클래스 로더가 정적 변수 또는 스레드 등에 의해 참조되고 있으면 GC 대상이 안 됨
- 그 결과, 클래스 정보(Metaspace)가 계속 메모리에 남아 있음 → OutOfMemoryError 발생
예: Spring의 Bean, 정적 필드, ThreadLocal 등이 참조를 유지하고 있으면 문제가 됩니다.
7. 실무에서의 활용 팁
- GC 튜닝 시 Heap 크기 조정: -Xms, -Xmx
- Metaspace OOM 방지: XX:MaxMetaspaceSize=256m 등 명시
- 스레드 수가 많아질 경우 Stack 메모리 확인: -Xss
- 클래스 로더 캐시 누수 점검: Fat Jar, 리플렉션 등
JVM 옵션으로 실행 시점에 명령어로 설정합니다.
💻 사용 예
java -XX:MaxMetaspaceSize=256m -jar myapp.jar
//java 자바 애플리케이션을 실행하는 명령어
//-XX:MaxMetaspaceSize=256m Metaspace 최대 크기를 256MB로 제한
//-jar myapp.jar myapp.jar이라는 JAR 파일을 실행하라는 명령
//-XX:InitialMetaspaceSize=64m 시작 시 할당하는 metaspace 크기
//-XX:+PrintGCDetails GC 로그에서 metaspace 사용량 확인 가능
- 또는 IDE (IntelliJ, Eclipse)의 Run/Debug Configuration > VM Options에 추가
- 또는 JAVA_OPTS, JVM_OPTS 환경변수로 지정 가능
💡 설정 이유:
- Metaspace는 기본적으로 OS 메모리를 무제한으로 쓰려 함
- 서버에서는 메모리 누수 방지를 위해 적절히 제한하는 것이 중요
마치며
📂 language / java 카테고리에서는 다음 내용을 이어서 다룹니다:
- JVM GC 동작 방식 (Minor GC, Major GC, Full GC)
- GC 튜닝 전략과 도구 (jstat, jvisualvm)
- 메모리 누수 감지와 실전 디버깅
JVM 메모리 구조는 성능을 결정짓는 핵심입니다.
Heap, Stack, Metaspace의 구조와 차이를 명확히 이해하면,
애플리케이션의 안정성과 확장성을 한 단계 높일 수 있습니다.
📌 이전 글 다시보기
👉 자바 플랫폼이란? – JVM, JDK, JRE로 이해하는 실행 환경의 구조
📌 다음 글 미리보기
👉 GC의 종류와 튜닝 전략 (G1GC, ZGC, ParallelGC)
📚 Java_runtime 시리즈 전체 보기
👉 https://jobreview.tistory.com/category/platform_infra_cloud/java_runtime
📌 바로보러가기 👉 자바 애플리케이션 구동과 메모리 로딩 과정
'platform_infra_cloud > java_runtime' 카테고리의 다른 글
Java 애플리케이션 로그 분석 흐름 가이드 (0) | 2025.04.19 |
---|---|
java -jar 실행 시 환경 변수 구성법 가이드 (0) | 2025.04.19 |
Spring Boot에서 JVM 설정 최적화 가이드 (0) | 2025.04.19 |
GC의 종류와 튜닝 전략 (G1GC, ZGC, ParallelGC) (0) | 2025.04.19 |
자바 플랫폼이란? – JVM, JDK, JRE로 이해하는 실행 환경의 구조 | Java 플랫폼의 구조와 실행 흐름 (0) | 2025.04.09 |
댓글