JVM은 자바 코드를 어떻게 실행할까?
JVM이란?
JVM(Java Virtual Machine)은 Java Bytecode를 실행하기 위한 가상 머신이다.
조금 더 정확히 말하면 JVM은 하나의 특정 프로그램을 의미하기보다 Bytecode를 어떻게 실행해야 하는지 정의한 명세에 가깝다. 이를 실제로 구현한 JVM 중 대표적인 것이 HotSpot JVM이다.
Java 소스 코드는 CPU가 직접 실행하지 않는다.
public class Main {
public static void main(String[] args) {
System.out.println("Hello JVM");
}
}
위 코드를 javac로 컴파일하면 Main.class 파일이 생성된다.
javac Main.java
.class 파일 안에는 특정 CPU를 위한 기계어가 아니라 Java Bytecode가 들어 있다.
전체 실행 과정은 다음 흐름으로 이해할 수 있다.

Java가 플랫폼 독립적이라고 불리는 이유도 여기에 있다.
Windows에서는 Windows용 JVM이 Bytecode를 실행하고, macOS에서는 macOS용 JVM이 동일한 Bytecode를 실행한다.
즉 Java Bytecode가 운영체제에 맞춰 변경되는 것이 아니라, 각 운영체제에 맞는 JVM 구현체가 같은 Bytecode를 실행하는 구조이다.
JVM 내부에서는 어떤 일이 일어날까?
JVM의 핵심 구조는 크게 세 부분으로 볼 수 있다.
- Class Loader
- Runtime Data Area
- Execution Engine
Class Loader
Java 프로그램을 실행한다고 모든 .class 파일을 처음부터 메모리에 올리는 것은 아니다.
필요한 클래스가 생기면 Class Loader가 해당 클래스의 Bytecode를 JVM으로 불러온다.
클래스를 불러온 뒤에는 Bytecode가 JVM 규칙에 맞는지 검증하고, 메모리를 준비하며, 클래스가 실제로 사용될 수 있도록 초기화하는 과정이 진행된다.
즉 Class Loader는 단순히 파일을 읽는 역할만 하는 것이 아니라 클래스를 JVM이 실행할 수 있는 상태로 만드는 역할까지 담당한다.
Runtime Data Area
JVM은 프로그램을 실행하면서 필요한 데이터를 여러 메모리 영역에 나누어 관리한다.
그중 Java를 공부하면서 가장 자주 접하는 영역이 Stack과 Heap이다.
다음 코드를 보자.
Person p1 = new Person("Kim");
Person p2 = p1;
p2.setName("Lee");
System.out.println(p1.getName()); // Lee
new Person("Kim")으로 생성된 객체는 Heap에 위치한다.
반면 메서드 내부의 지역 변수인 p1, p2는 Stack Frame 안에서 관리되며, 두 변수는 Heap에 존재하는 동일한 Person 객체를 참조한다.
그래서 p2를 통해 객체의 이름을 변경했지만 p1에서도 변경된 값이 확인된다.
이 구조는 Java에서 객체를 이해할 때 중요하다.

단순히 객체는 Heap, 지역 변수는 Stack이라고 외우는 것보다, Stack에 있는 참조값이 Heap의 객체를 가리킨다고 이해하는 것이 더 정확하다. JVM에는 이외에도 클래스 정보 등을 관리하는 Method Area, 현재 실행 중인 명령의 위치를 관리하는 PC Register 등의 영역이 존재한다.
Bytecode는 어떻게 실행될까?
메모리에 올라온 Bytecode를 실제로 실행하는 역할은 Execution Engine이 담당한다.
여기에서 중요한 것이 Interpreter와 JIT Compiler이다.
Interpreter
Interpreter는 Bytecode 명령을 하나씩 읽어 실행한다. 프로그램을 바로 실행할 수 있다는 장점이 있지만, 같은 코드가 수천 번 반복된다면 계속 Bytecode를 해석해야 하는 문제가 생긴다. 서버 애플리케이션처럼 오랫동안 실행되는 프로그램에서는 이런 비용이 무시하기 어렵다. 그래서 JVM에는 JIT Compiler(Just-In-Time Compiler)가 존재한다.
JIT Compiler
JVM은 프로그램을 실행하면서 어떤 코드가 자주 실행되는지에 대한 정보를 수집한다. 반복적으로 실행되는 메서드나 코드 영역이 발견되면 JIT Compiler가 해당 Bytecode를 Native Code로 컴파일한다. 이렇게 자주 실행되는 코드 영역을 흔히 Hot Spot이라고 한다.
한번 Native Code로 컴파일된 코드는 매번 Interpreter가 Bytecode를 다시 해석할 필요 없이 실행할 수 있다. 즉 JVM은 처음부터 모든 코드를 Native Code로 컴파일하는 것이 아니라, 실행하면서 정보를 수집하고, 자주 실행되는 코드를 선택적으로 최적화한다.
이 점이 JVM의 중요한 특징 중 하나이다. 그래서 Java 프로그램은 단순히 “Bytecode를 한 줄씩 해석해서 실행하는 프로그램”이라고 설명하기에는 부족하다. Java 소스 코드는 Bytecode로 컴파일되고, JVM은 Interpreter와 JIT Compiler를 함께 사용해 이를 실행한다.
Garbage Collector는 무엇을 정리할까?
Java에서는 개발자가 객체의 메모리를 직접 해제하지 않는다.
Heap에 만들어진 객체 중 더 이상 사용할 수 없는 객체는 Garbage Collector(GC)가 찾아서 정리한다.
예를 들어 다음 코드가 있다고 하자.
Person person = new Person("Kim");
person = null;
person = null이 실행되었다고 객체가 즉시 메모리에서 삭제되는 것은 아니다. 다른 곳에서도 해당 객체에 접근할 수 없다면 GC가 이후 해당 객체를 정리할 수 있는 상태가 된다. JVM은 객체를 단순히 “참조 개수가 몇 개인가”만 보고 판단하지 않는다. 핵심은 GC Root에서 해당 객체까지 도달할 수 있는가이다. GC Root에서 더 이상 접근할 수 없는 객체를 Garbage로 판단할 수 있는데, 이를 Reachability라고 한다. 이 원리를 기반으로 JVM은 사용하지 않는 객체를 찾아 Heap 공간을 다시 확보한다.
JDK, JRE, JVM의 관계
마지막으로 자주 헷갈리는 세 개념을 정리하면 다음과 같다.
| 구분 | 역할 |
| JVM | Java Bytecode를 실행하는 가상 머신 |
| JRE | Java 프로그램 실행에 필요한 환경 |
| JDK | Java 개발에 필요한 컴파일러와 도구를 포함한 개발 환경 |
우리가 Java 개발을 위해 설치하는 것은 일반적으로 JDK이다.
JDK 안에는 javac 같은 개발 도구와 Java 프로그램을 실행하는 데 필요한 구성 요소가 포함되어 있다.
정리
JVM의 핵심은 단순히 Java 코드를 실행하는 데 있지 않다. Java 소스 코드는 먼저 Bytecode로 컴파일된다. Class Loader는 필요한 클래스를 JVM으로 불러오고, Runtime Data Area에서는 객체와 메서드 실행 정보를 관리한다. Execution Engine은 Interpreter를 이용해 Bytecode를 실행하면서 런타임 정보를 수집하고, 자주 실행되는 코드는 JIT Compiler를 이용해 Native Code로 최적화한다. 그리고 Heap에 생성된 객체 중 더 이상 접근할 수 없는 객체는 Garbage Collector가 정리한다. 결국 JVM은 Bytecode 실행, 메모리 관리, 런타임 최적화를 담당하는 Java 실행 환경의 핵심이라고 볼 수 있다.