JavaMedium#51
JVM의 구조(메모리 영역)를 설명해주세요.
#Java#JVM#메모리#핵심개념
힌트
Method Area, Heap, Stack, PC Register, Native Method Stack을 떠올려보세요.
정답 및 해설
JVM의 구조(메모리 영역)를 설명해주세요.
JVM(Java Virtual Machine)은 Java 바이트코드를 실행하는 가상 머신으로, "Write Once, Run Anywhere"를 가능하게 하는 핵심 요소입니다. JVM은 런타임 시 다양한 메모리 영역을 사용하며, 각 영역은 특정 목적에 맞게 설계되어 있습니다. JVM 메모리 구조를 이해하면 OutOfMemoryError 디버깅, 성능 튜닝, GC(가비지 컬렉션) 동작 이해에 도움이 됩니다.
JVM 전체 구조
┌─────────────────────────────────────────────────────────────────┐
│ JVM │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 런타임 데이터 영역 │ │
│ │ ┌─────────────┐ ┌─────────────────────────────────┐ │ │
│ │ │ Method Area │ │ Heap │ │ │
│ │ │ (공유) │ │ ┌──────────────┐ ┌──────────┐ │ │ │
│ │ │ - 클래스 정보│ │ │Young Gen │ │ Old Gen │ │ │ │
│ │ │ - static 변수│ │ │ Eden|S0|S1 │ │ │ │ │ │
│ │ │ - 상수 풀 │ │ └──────────────┘ └──────────┘ │ │ │
│ │ └─────────────┘ └─────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────┐ ┌─────────┐ ┌──────────────────┐ │ │
│ │ │ JVM Stack │ │ PC │ │ Native Method │ │ │
│ │ │ (스레드별) │ │Register │ │ Stack │ │ │
│ │ │ [Frame] │ │(스레드별)│ │ (스레드별) │ │ │
│ │ └──────────────┘ └─────────┘ └──────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Class Loader │ │ 실행 엔진 │ │ Native Method │ │
│ │ Subsystem │ │ (JIT 컴파일러│ │ Interface (JNI) │ │
│ │ │ │ + 인터프리터)│ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
메서드 영역 (Method Area)
특징
- 모든 스레드가 공유하는 영역
- JVM 시작 시 생성, JVM 종료 시 해제
- Java 8부터 "Metaspace"로 변경 (PermGen 제거)
- PermGen: Heap의 일부였으며 크기 고정 → OutOfMemoryError: PermGen space 발생
- Metaspace: Native Memory 사용, 동적으로 크기 조정
저장 내용
// 아래 코드 실행 시 Method Area에 저장되는 것들:
public class User {
// static 변수 → Method Area에 저장
private static int count = 0;
private static final String TYPE = "USER"; // 상수 풀(Constant Pool)에 저장
// 인스턴스 변수 → 메서드 영역에 필드 정보만, 실제 값은 Heap에
private String name;
private int age;
// 메서드 바이트코드 → Method Area에 저장
public String getName() { return name; }
// 클래스 메타데이터:
// - 클래스 이름, 접근 제한자
// - 부모 클래스 참조
// - 구현한 인터페이스 목록
// - 필드/메서드 정보
}
// 런타임 상수 풀 (Runtime Constant Pool)
// - 리터럴 값 ("hello", 42 등)
// - 심볼릭 참조 (클래스/메서드/필드 이름)
String s1 = "hello" // 상수 풀에서 참조
String s2 = "hello" // 동일한 상수 풀 항목 참조
System.out.println(s1 == s2) // true (같은 참조)
Metaspace 튜닝
# JVM 옵션
-XX:MetaspaceSize=256m # Metaspace 초기 크기
-XX:MaxMetaspaceSize=512m # Metaspace 최대 크기
# 설정하지 않으면 사용 가능한 Native Memory 한도까지 자동 확장
# PermGen 시절 (Java 8 이전)
-XX:PermSize=128m # 초기 크기 (지금은 무시됨)
-XX:MaxPermSize=256m # 최대 크기 (지금은 무시됨)
힙 (Heap)
특징
- 모든 스레드가 공유하는 영역
- 인스턴스 객체와 배열이 저장되는 곳
- GC(가비지 컬렉션)의 대상 - 더 이상 참조되지 않는 객체 수거
- Young Generation + Old Generation으로 구성
Heap 구조 상세
Heap
├── Young Generation (새로 생성된 객체들)
│ ├── Eden Space : 새 객체가 최초 생성되는 공간
│ ├── Survivor 0 (S0) : Minor GC 생존 객체 임시 보관
│ └── Survivor 1 (S1) : Minor GC 생존 객체 임시 보관
│
└── Old Generation (오래된 객체들)
└── Tenured Space : Young Gen에서 오래 살아남은 객체
// 객체 생성 - Heap에 저장됨
User user = new User("김철수", 25) // new → Eden Space에 객체 생성
int[] arr = new int[100] // 배열도 Heap에 저장
// 참조 변수는 Stack에, 실제 객체 데이터는 Heap에
// Stack: [user → 참조값(주소)] ------→ Heap: { name:"김철수", age:25 }
// 객체가 참조를 잃으면 GC 대상
user = null // Heap의 User 객체가 GC 대상이 됨
Heap 메모리 설정
# JVM Heap 크기 설정
-Xms512m # 초기 Heap 크기 (최소)
-Xmx2g # 최대 Heap 크기
-Xmn256m # Young Generation 크기
# 권장: -Xms와 -Xmx를 동일하게 설정 → GC 오버헤드 감소
java -Xms2g -Xmx2g -jar myapp.jar
# Young/Old Generation 비율
-XX:NewRatio=2 # Old:Young = 2:1 (Young = 전체의 1/3)
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1:1 (Eden = Young의 8/10)
OutOfMemoryError 유형
java.lang.OutOfMemoryError: Java heap space
→ Heap 공간 부족. -Xmx 증가 또는 메모리 누수 확인
java.lang.OutOfMemoryError: GC overhead limit exceeded
→ GC가 너무 자주 실행되어 작업 시간의 98%를 GC에 사용
java.lang.OutOfMemoryError: Metaspace
→ 클래스가 너무 많이 로드됨. -XX:MaxMetaspaceSize 증가
java.lang.OutOfMemoryError: Direct buffer memory
→ NIO Direct Buffer 부족. -XX:MaxDirectMemorySize 증가
JVM 스택 (JVM Stack)
특징
- 각 스레드마다 별도의 Stack 생성
- 메서드 호출 시 스택 프레임(Stack Frame) 생성, 반환 시 제거
- 스레드 안전 (다른 스레드와 공유하지 않음)
StackOverflowError: 재귀 호출이 너무 깊을 때 발생
스택 프레임 구성
스레드의 JVM Stack:
┌─────────────────────────────────────┐
│ Stack Frame 3: method3() │ ← 현재 실행 중
│ ├── 지역 변수 배열 (Local Variables)│
│ │ [0] this, [1] param1, [2] x │
│ ├── 피연산자 스택 (Operand Stack) │
│ └── 현재 클래스의 런타임 상수 풀 참조│
├─────────────────────────────────────┤
│ Stack Frame 2: method2() │
│ ├── 지역 변수: [0] this, [1] y │
│ └── 피연산자 스택 │
├─────────────────────────────────────┤
│ Stack Frame 1: method1() │ ← 처음 호출된 메서드
│ └── ... │
└─────────────────────────────────────┘
public class StackExample {
public static void main(String[] args) {
// 스택 프레임 1: main()
int x = 10; // 지역 변수 → Stack에 저장
String name = "철수" // 참조값 → Stack에, 실제 문자열 → Heap에
method1(x); // method1 호출 → 새 스택 프레임 생성
} // main 반환 → 스택 프레임 1 제거
static void method1(int n) {
// 스택 프레임 2: method1()
int result = n * 2; // 지역 변수 → Stack
method2(result);
} // 반환 → 스택 프레임 2 제거
static void method2(int value) {
// 스택 프레임 3: method2()
System.out.println(value);
}
}
// StackOverflowError 예시
static void infiniteRecursion() {
infiniteRecursion() // 종료 조건 없는 재귀 → StackOverflowError
}
스택 크기 설정
# 기본 스택 크기: 플랫폼에 따라 다름 (일반적으로 512KB~1MB)
-Xss512k # 스레드당 스택 크기 512KB
-Xss2m # 스레드당 스택 크기 2MB
# 깊은 재귀가 필요한 경우 증가
# 단, 메모리를 많이 사용하는 멀티스레드 환경에서는 주의
PC 레지스터 (Program Counter Register)
특징
- 각 스레드마다 별도 존재
- 현재 스레드가 실행 중인 JVM 명령어(bytecode)의 주소 저장
- 네이티브 메서드 실행 중에는 undefined
// PC 레지스터 동작 (내부적으로)
// 바이트코드:
// 0: iload_1 ← PC = 0
// 1: iload_2 ← PC = 1
// 2: iadd ← PC = 2
// 3: istore_3 ← PC = 3
// 4: return
// 멀티스레드 환경에서 컨텍스트 스위칭 시
// 각 스레드의 PC 레지스터가 다음 실행할 명령어 주소를 기억함
// Thread A: PC = 2 (iadd 실행 중)
// Thread B: PC = 15 (다른 명령어 실행 중)
// → 컨텍스트 스위치 후 각 스레드가 올바른 위치에서 재개 가능
네이티브 메서드 스택 (Native Method Stack)
특징
- 각 스레드마다 별도 존재
- Java가 아닌 C/C++로 작성된 네이티브 메서드 실행에 사용
- JNI(Java Native Interface)를 통해 호출
// 네이티브 메서드 선언
public class NativeExample {
// native 키워드 - JVM Stack 아닌 Native Method Stack 사용
public native int nativeCalculation(int a, int b)
static {
System.loadLibrary("myNativeLib") // .so / .dll 파일 로드
}
}
// java.lang.Object의 메서드들도 native
public final native Class<?> getClass()
public native int hashCode()
protected native Object clone() throws CloneNotSupportedException
스레드 공유 vs 스레드 독립 요약
// 공유 영역 (스레드 안전 이슈 발생 가능)
// - Method Area: static 변수, 클래스 메타데이터
// - Heap: 객체 인스턴스
// 예: static 변수는 모든 스레드가 공유 → 동기화 필요
class Counter {
private static int count = 0 // Heap에? NO, Method Area에
// 동기화 없이는 스레드 안전하지 않음
public static synchronized void increment() {
count++
}
}
// 스레드별 독립 영역 (스레드 안전)
// - JVM Stack: 지역 변수, 매개변수
// - PC Register: 현재 실행 주소
// - Native Method Stack: 네이티브 메서드 호출
// 지역 변수는 스레드별 Stack에 있으므로 동기화 불필요
void processData(int value) {
int localVar = value * 2 // Stack에 저장 → 스레드 안전
}
JVM 메모리 모니터링
# JVM 플래그로 GC 로그 확인
java -verbose:gc -XX:+PrintGCDetails -jar myapp.jar
# jstat으로 실시간 메모리 모니터링
jstat -gc <PID> 1000 # 1초 간격으로 GC 통계 출력
# S0C S1C S0U S1U EC EU OC OU MC MU ...
# Survivor0 Capacity, Usage / Eden Capacity, Usage / Old ...
# jmap으로 힙 덤프 생성
jmap -dump:format=b,file=heapdump.hprof <PID>
# jvisualvm / JMC (Java Mission Control)로 GUI 모니터링
정리 표
| 영역 | 스레드 공유 | 저장 내용 | GC 대상 | 에러 유형 |
|---|---|---|---|---|
| Method Area (Metaspace) | 공유 | 클래스 메타데이터, static 변수, 상수 풀 | 일부 | OutOfMemoryError: Metaspace |
| Heap (Eden, Survivor, Old) | 공유 | 인스턴스 객체, 배열 | 주 대상 | OutOfMemoryError: Java heap space |
| JVM Stack | 스레드별 독립 | 스택 프레임(지역 변수, 매개변수) | 아님 | StackOverflowError |
| PC Register | 스레드별 독립 | 현재 실행 중인 바이트코드 주소 | 아님 | - |
| Native Method Stack | 스레드별 독립 | 네이티브 메서드 호출 정보 | 아님 | StackOverflowError |