[JAVA] JVM의 동작원리
JVM이란?
JVM(Java Virtual Machine)은 자바 바이트코드를 운영체제에 독립적으로 실행할 수 있도록 해주는 가상 실행 환경으로, 클래스 로딩부터 메모리 관리, 실행까지 전 과정을 담당하는 자바의 핵심 엔진입니다.
- 사용자가 작성한 코드는 Javac(자바 컴파일러)를 통해 JVM이 이해할 수 있는 바이트코드로 변환됩니다.
- 변환된 바이트코드는
.class
파일로 저장되며, 운영체제에 관계없이 JVM에서 실행할 수 있습니다. - 하지만 바이트코드는 기계어가 아니기 때문에, JVM이 실행 시 운영체제에 맞는 기계어로 변환하여 실행합니다.
JVM의 구성요소
- 클래스 로더 (Class Loader)
- 실행 엔진 (Execution Engine)
- 런타임 데이터 영역 (Runtime Data Area)
- JNI (Java Native Interface)
- 네이티브 메서드 라이브러리 (Native Method Library)
클래스 로더 (Class Loader)
JVM은 .class
파일을 읽어 바이트코드를 JVM 메모리(Method Area)에 로드합니다.
이때 모든 클래스를 한 번에 올리는 것이 아니라, 애플리케이션이 필요로 하는 시점에 동적으로 할당합니다.
클래스 로딩과 Linking, Initialization 과정
클래스 로딩 과정
- Loading (로드):
.class
파일을 JVM 메모리로 로드 - Linking (링크): JVM에서 실행할 수 있도록 준비하는 단계
- Verifying (검증):
.class
파일이 JVM 명세에 맞게 구성되었는지 확인 - Preparing (준비): static 변수 등을 위한 메모리 공간 할당 및 기본값 설정
- Resolving (분석): 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변환
- Verifying (검증):
- Initialization (초기화): static 변수에 실제 값 할당 등 초기화 수행
심볼릭 vs 다이렉트 레퍼런스
- 심볼릭 레퍼런스: “어디 있는지 모른 채 이름만 알고 있는 상태” (예: 변수명, 클래스명)
- 다이렉트 레퍼런스: JVM이 메모리 주소를 확인하고 저장 → 즉시 접근 가능
- 처음엔 “집앞 카페”라고만 알고 있다가, 한 번 가본 후엔 “정확한 주소”로 빠르게 찾아가는 것과 같음
실행 엔진 (Execution Engine)
JVM은 바이트코드를 실제 기계어로 변환하여 실행하기 위해 인터프리터(Interpreter)와 JIT(Just-In-Time) 컴파일러 방식을 함께 사용합니다.
실행 방식
- 인터프리터: 바이트코드를 한 줄씩 해석하여 실행
(초기 실행 속도는 빠르지만, 반복 실행 시 느림) - JIT 컴파일러: 자주 실행되는 코드를 네이티브 코드로 변환하여 캐싱 → 속도 최적화
JIT의 최적화 기법
- 인라이닝: 자주 호출되는 메서드를 직접 삽입해 호출 오버헤드 제거
- 루프 언롤링: 반복문을 펼쳐 반복 횟수를 줄이고 분기 비용 감소
- 동적 디스패치 제거: 메서드 호출을 정적으로 고정해 불필요한 동적 호출 제거
실행 흐름
바이트코드 → 인터프리터 실행 → JIT 컴파일러가 반복 코드 최적화 → 네이티브 코드로 변환 → 실행 속도 향상
가비지 컬렉션 (GC)
JVM은 사용하지 않는 객체를 자동으로 제거하여 메모리를 관리합니다.
GC 주요 개념
- Young Generation / Old Generation
- Minor GC: Young Gen에서 수행, 빠름
- Major GC: Old Gen에서 수행, 느림
- Full GC: 전체 Heap 대상, Stop-The-World 발생 가능
GC 튜닝
-Xms
: 초기 힙 크기-Xmx
: 최대 힙 크기-XX:+UseG1GC
: G1 GC 사용-XX:+PrintGCDetails
: GC 상세 로그 출력
GC는 JVM 성능의 핵심 요소이며, 최근에는 멈추지 않는 초저지연 GC가 트렌드입니다.
런타임 데이터 영역(Runtime Data Area)
JVM은 실행 중 다양한 메모리 영역을 활용합니다.
메서드 영역
- 클래스 메타정보, static 변수, 런타임 상수 풀 등 저장
- Java 8부터는 Metaspace 사용
- 과도하게 사용되면
OutOfMemoryError
발생
런타임 상수 풀 예시
1
2
3
String a = "Java";
String b = "Java";
System.out.println(a == b); // true
1
2
3
4
String a = new String("Java");
String b = "Java";
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
1
2
3
4
String a = new String("Java");
String b = a.intern();
String c = "Java";
System.out.println(b == c); // true
힙 영역
- 모든 객체와 배열 저장
- GC가 대상 탐지 및 제거
- Young Gen: Eden + Survivor 0/1, Minor GC
- Old Gen: 장기 객체 저장, Major GC
스택 영역
- 스레드별 독립적, LIFO 구조
- 메서드 호출 시 프레임 생성/제거
1
String a = new String("Java"); // Heap에 객체, Stack에 참조 변수
PC 레지스터
- 현재 실행 중인 명령어의 주소 저장
- 스레드마다 존재
1
2
System.out.println("Hi");
// getstatic → ldc → invokevirtual 순으로 PC 레지스터가 위치 추적
네이티브 메서드 스택
- C, C++ 등의 네이티브 메서드 호출 시 사용
JNI (Java Native Interface)
- 자바 ↔ 네이티브 간의 브릿지 역할
복잡성, 디버깅 난이도, 충돌 가능성 주의
런타임 데이터 영역(Runtime Data Area)의 흐름
1
2
3
4
5
6
7
8
9
class Test {
static int staticVar = 10; // 메서드 영역에 저장
int instanceVar = 5; // Heap에 저장
void greet(String name) { // 메서드 정의는 메서드 영역
String msg = "Hello " + name; // 지역 변수는 Stack에 저장
System.out.println(msg); // 메서드 호출 → 스택 프레임 생성
}
}
-
JVM이
Test
클래스를 로딩
staticVar
,greet()
메서드 바이트코드 등은 메서드 영역(Method Area) 에 저장됨
static
이기 때문에 메서드 영역에 저장 -
new Test()
객체 생성
instanceVar
는 인스턴스 변수로, 객체와 함께 Heap 영역에 저장됨
객체가 생성될 때마다 새로 생기므로 Heap에 저장 -
hello.greet("JVM")
호출
"JVM"
(매개변수)와"Hello JVM"
(지역 변수msg
)는 Stack 영역에 저장됨
System.out.println()
도 호출되며 스택 프레임(Stack Frame) 이 하나 더 생성됨 -
메서드 실행 종료
greet()
와println()
의 스택 프레임은 제거됨
Heap에 남은hello
객체는 더 이상 참조되지 않으면 GC 대상이 될 수 있음
✅ 기억해야 할 것
JVM 명세에 따르면 Method Area는 논리적으로 힙의 일부로 간주되기도 하지만, 클래스 정보와 실행 코드, 상수 풀 등을 저장하며 일반적인 객체를 저장하는 힙과는 기능적으로 명확히 다르다. 이러한 차이로 인해 JVM 메모리 구조도에서는 보통 Method Area를 힙과 분리된 영역으로 시각화한다.
🔗 JVM Spec §2.5.4 - Method Area
변수 종류 | 저장 위치 | 설명 |
---|---|---|
static 변수 | 메서드 영역 | 클래스 로딩 시 1번만 생성, 인스턴스 공유 |
인스턴스 변수 | Heap 영역 | 객체 생성 시마다 생성 |
지역 변수 | Stack 영역 | 메서드 호출 시 생성, 종료 시 제거 |