자바 가상 머신
자바 가상 머신(JVM)은 자바 바이트 코드(.class)를 OS의 종류에 관계없이 실행할 수 있게 해주는 소프트웨어 기반의 가상 머신이다. 이는 자바의 가장 큰 특징인 플랫폼 독립성을 가능하게 하는 핵심 요소이다. JVM은 크게 클래스 로더, 런타임 데이터 영역, 실행 엔진, JNI와 같이 네 가지 구성 요소로 나뉜다.
클래스 로더
.class 파일(바이트 코드)을 JVM의 메소드 영역에 로드하고 사용할 수 있도록 준비하는 역할을 담당한다.
| 단계 | 설명 | 상세 내용 |
|---|---|---|
| 로딩(Loading) | 클래스 파일을 읽어와 JVM의 메소드 영역에 저장한다. | 클래스의 메타 데이터, 슈퍼 클래스 정보, 인터페이스 정보 등을 JVM 메모리에 올린다. |
| 링크(Linking) | 로드된 클래스 파일을 실행 가능하도록 연결하는 과정이다. | 세부적으로 검증(Verification), 준비(Preparation), 해결(Resolution) 단계를 거친다. |
| 초기화(Initialization) | 클래스가 실제로 실행될 준비를 완료하는 단계다. | static 변수들을 초기값을 초기화하고, 클래스에 정의된 초기화 블록을 실행한다. 이 단계는 스레드 안전(Thread-Safe)으로 진행된다. |
클래스 로더의 종류는 다음과 같다.
-
부트스트랩 클래스 로더(Bootstrap Class Loader): JVM 자체의 핵심 라이브러리를 로드한다.
-
확장 클래스 로더(Extension Class Loader): 확장 라이브러리를 로드한다.
-
애플리케이션 클래스 로더(Application Class Loader): 사용자 애플리케이션의 클래스 경로에 있는 클래스 파일을 로드한다.
런타임 데이터 영역
프로그램 실행을 위해 JVM이 사용하는 메모리 영역으로 스레드 공유 영역과 스레드 독립 영역으로 나뉜다.
| 영역 | 저장 내용 | 공유 여부 | 상세 역할 |
|---|---|---|---|
| 메소드 영역(Method Area) | 클래스 수준 정보(클래스 이름, 필드/메소드 정보, 런타임 상수 풀 등) | 모든 스레드 공유 | 클래스 로더가 로드한 클래스 메타 데이터를 저장한다. |
| 힙 영역(Heap Area) | new 키워드로 생성된 객체와 배열 |
모든 스레드 공유 | 가비지 컬렉션의 주요 대산이 되는 영역이다. |
| JVM 스택(JVM Stack) | 메소드 호출 시 생성되는 스택 프레임 | 스레드 독립적 | 프레임 내에 지역 변수, 파라미터, 중간 연산 결과, 반환 주소 등을 저장한다. 메소드 호출마다 프레임이 쌓이고, 반환 시 제거된다. |
| PC 레지스터(Program Counter) | 현재 실행 중인 JVM 명령의 주소 | 스레드 독립적 | 각 스레드가 어디까지 실행했는가를 기록하여 스레드 전환 시 작업을 재개할 수 있게 한다. |
| 네이티브 메소드 스택 | 네이티브 메소드 실행을 위한 스택 | 스레드 독립적 | JNI를 통해 자바 외 언어(C/C++)로 작성된 코드가 실행될 때 사용되는 스택이다. |
실행 엔진
클래스 로더가 로드한 바이트 코드를 실행하는 역할을 담당한다.
| 구성 요소 | 역할 | 상세 설명 |
|---|---|---|
| 인터프리터(Interpreter) | 바이트 코드를 한 줄씩 읽어 해석하고 실행한다. | 실행 속도는 느리지만, 모든 코드를 실행하기 전에 컴파일할 필요가 없어 유연하다. |
| JIT(Just-In-Time) 컴파일러 | 자주 실행되는 코드(핫스팟, Hotspot)를 감지하여 네이티브 코드(기계어)로 변환 후 캐싱한다. | 캐싱된 네이티브 코드를 재사용하여 실행 속도를 향상시킨다. 인터프리터의 유연성과 컴파일러의 속도를 결합한다. |
| 가비지 컬렉터(Garbage Collector) | 힙 영역에서 더 이상 참조되지 않는 객체들을 탐지하고 제거하여 메모리를 자동으로 회수한다. | 개발자의 메모리 관리 부담을 줄여주며, 메모리 누수를 방지한다. |
JNI
JNI(Java Native Interface) 및 네이티브 라이브러리는 자바 애플리케이션이 네이티브 메소드 라이브러리와 상호 작용할 수 있게 해주는 표준 인터페이스로 자바의 성능 병목 구간이나 OS 종속적인 기능을 네이티브 코드로 구현하고 호출할 수 있게 해준다.
자바 가상 머신의 동작 흐름
전체 동작 과정은 컴파일, 클래스 로딩, 실행 및 GC 단계로 요약할 수 있다.
-
자바 소스 컴파일: 개발자가 작성한
.java파일을 자바 컴파일러(javac)가 자바 바이트 코드(.class)로 변환한다. -
클래스 로더: 클래스 로더가 필요한
.class파일을 JVM의 메소드 영역으로 가져오는 로딩 과정과, 검증을 통해 바이트 코드의 안전성을 확인하고, 준비 단계에서static변수에 메모리를 할당하는 링크 과정 그리고static변수에 실제 값을 할당하고,static블록을 실행하여 클래스를 사용할 준비를 마치는 초기화 과정을 거친다. -
메모리 배치 및 스레드 준비: 로드된 클래스 정보를 메소드 영역에 위치시키고 프로그램 시작 시 main() 메소드를 실행할 메인 스레드를 생성한다. 마지막으로 각 스레드에 JVM 스택 및 PC 레지스터가 독립적으로 할당된다.
-
실행: 실행 엔진이 메소드 영역의 바이트 코드를 읽어 실행한다. 메소드 호출 시 JVM 스택에 스택 프레임이 쌓이고, 해당 메소드의 명령어 위치는 PC 레지스터에 기록된다. 인터프리터가 바이트 코드를 실행하며, JIT 컴파일러는 반복 코드를 네이티브 코드로 최적화한다.
-
GC:
new연산자로 생성된 객체를 힙 영역에 할당하고, 가비지 컬렉터가 주기적으로 힙 영역을 탐색하여 더 이상 참조되지 않는 객체를 안전하게 제거하여 메모리를 회수한다.
자바 가상 머신의 장점 및 의의
JVM은 자바 언어의 핵심적인 설계 요소다.
-
플랫폼 독립성(WORA): 바이트 코드는 OS에 종속적이지 않으며, JVM이 설치된 환경이라면 어디서든 실행 가능하다.
-
자동 메모리 관리: GC 덕분에 개발자가 수동으로 메모리를 관리할 필요가 없어지며, 메모리 누수의 위험이 크게 줄어든다.
-
성능 최적화: JIT 컴파일러를 통해 프로그램 실행 중 동적 최적화가 가능하여, 초기에는 인터프리터 방식이지만 장시간 실행 시 네이티브 코드에 준하는 성능을 낼 수 있다.
-
보안 강화: 클래스 로더의 검증 단계를 통해 유효하지 않거나 악의적인 바이트 코드가 JVM에 로드되는 것을 방지한다.
-
하드웨어 접근 가능성: JNI를 통해 네이티브 코드와 연동할 수 있어, 성능에 민감한 작업이나 하드웨어/OS 종속 기능을 구현할 수 있다.