엔진을 만드는 사람이 되기 위해선 엔진의 동작 원리를 이해해야 합니다.
마찬가지로 자바를 사용하는 개발자라면, 단순히 개발하는 것을 넘어 자바의 실행 환경은 JVM의 동작 원리를 이해할 필요가 있습니다.
우리가 작성한 자바 코드는 바로 실행되는 것이 아니라 컴파일과 클래스 로딩, 메모리 할당, 실행 엔진을 거치는 일련의 과정을 통해 동작합니다. 이 과정은 평소에는 보이지 않지만 성능 문제나 메모리 누수, 예기치 못한 오류가 발생했을 때 직접적인 원인이 됩니다.
예를 들어, 객체를 많이 생성했을 때 왜 GC가 발생하는지,
동일한 코드인데도 실행 속도가 다른 이유는 무엇인지,
또는 OutOfMemoryError가 발생하는 근본적인 원인은 무엇인지와 같은 문제는 JVM의 동작을 이해하지 않으면 근본적으로 해결하기 어렵습니다.
따라서 JVM을 이해하는 것은 성능 최적화와 문제 해결 능력을 갖춘 엔지니어로 나아가기 위한 필수 과정입니다.
이 글에서는 JVM을 중심으로 JRE와 JDK의 구조와 역할을 함께 살펴보겠습니다.
자바 프로그램의 실행 과정
이전에 자바 프로그램이 실행되는 과정을 알아보겠습니다.
- 우리는 통합 개발 환경을 사용하여 자바 소스 코드(simple.java)를 작성합니다.
- 작성한 프로그램은 바이트 코드로 컴파일 되어야합니다. 자바 컴파일러 (javac)는 소스코드를 클래스 파일(simple.class)로 컴파일합니다.
- 이 클래스 파일은 JVM을 통해 모든 플랫폼/운영체제에서 실행될 수 있습니다.
이 과정에서 JVM은 바이트 코드를 기계가 실행할 수 있는 네이티브 코드로 변환합니다.

JDK (Java Development Kit) : 자바 개발 키트
우리는 자바 개발을 하기 위한 첫번째로 JDK를 다운로드합니다.
JDK는 말 그대로 자바 애플리케이션을 개발하기 위한 키트입니다.
여기에는 JRE, 컴파일러(javac), 디버거(jdb), 아카이버(jar), 그리고 Java 개발에 사용되는 기타 도구가 포함됩니다.
자바 코드를 작성하고 이를 컴퓨터가 이해할 수 있는 상태로 변환하려면 JDK가 반드시 필요합니다.
JDK는 아래와 같이 구성되어 있습니다.

JDK = JRE + 자바 애플리케이션을 개발하기 위한 도구들
JRE = JVM + 자바 애플리케이션을 실행하기 위한 라이브러리들
JRE(Java Runtime Environment) : 자바 런타임 환경
JRE는 앞에서 언급했듯, 자바로 작성된 애플리케이션을 실행하기 위해 필요합니다.
JRE는 자바 애플리케이션을 실행하는데 필요한 모든 것을 포함하고 있습니다.
여기엔 자바 표준 라이브러리가 포함됩니다. java.lang 패키지에 있는 String, Math, Exception, Throwable 같은 클래스들과, 모든 자바 클래스의 뿌리인 Object 클래스까지 포함됩니다. 리스트나 셋, 맵을 위한 java.util, 모든 종류의 입출력을 위한 java.io, 데이터베이스와 상호작용하기 위한 java.sql 등 수없이 많습니다.
이 모든 것들과 JVM이 JRE의 일부로서 내장되어 있습니다.
따라서 자바 애플리케이션을 실행하려면 컴퓨터에 JRE가 설치되어 있어야 합니다.
JRE는 JDK에 포함되어 다운로드하거나 JRE만 별도로 다운로드 할 수 있습니다. 자바 애플리케이션을 개발하는 것이 아닌 실행만이 목적인 서버라면 JRE만 설치하면 되는 것이지만, Java 11 버전 이후부터는 오라클 등 주요 벤더들이 JRE 배포판을 따로 내놓지 않고 JDK에 통합하는 추세입니다. 따라서 최근에는 JRE만 따로 설치하기보다 JDK를 설치하여 환경을 구성하는 경우가 많습니다.
JVM(Java Virtual Machine) : 자바 가상 머신
JRE의 가장 핵심적인 부분인 JVM입니다. JRE가 자바 실행 환경이라면, JVM(자바 가상 머신)은 자바 실행 주체라고 할 수 있습다.
앞에서 살펴본 자바 바이트코드를 실행을 담당하는 것이 JVM입니다.
JVM은 우리가 프로그램을 전혀 수정하지 않아도 윈도우, 리눅스, 맥 OS 등 어떤 플랫폼에서든 인터프리터 역할을 하며 프로그램이 원활하게 돌아가도록 보장해줍니다.
이는 하드웨어와 OS에 독립적인 인터페이스를 제공하여 "Write Once, Run Anywhere"를 실현합니다.
JVM 자체가 하나의 소프트웨어가 아니라 "자바 프로그램을 실행하려면 이렇게 만들어야 한다"라는 일종의 가이드라인(명세, Specification)입니다.
이 가이드라인을 바탕으로 각 제조사(벤더)가 실제로 만든 프로그램을 구현체라고 부릅니다. 우리가 가장 흔히 사용하는 오라클의 HotSpot이 대표적이며, 오픈소스 프로젝트인 OpenJDK, IBM의 J9 등이 있습니다. 어떤 구현체를 쓰더라도 JVM의 표준 명세를 따르기 때문에 자바 프로그램은 동일하게 동작할 수 있는 것입니다.
JVM의 아키텍처
아래 이미지는 JVM의 아키텍처와 주요 구성 요소를 보여줍니다.

1. 클래스 로더 (Class Loader)
클래스 로더 시스템은 주로 3가지 활동을 담당합니다.

- Loading(로딩) : .class 파일(.java 파일이 컴파일된 것)을 읽고 클래스 메타데이터를 메서드 영역에 저장합니다. 로드된 클래스를 나타내는 Class 객체를 힙에 생성합니다.
class Coffee{
static{
System.out.println("Coffee class is loaded by the JVM!");
}
public void drink(){
System.out.println("Method of Coffee class is executed.");
}
}
public class Test{
public static void main(String[] args) throws Exception{
System.out.println("Main method started.");
// Class.forName()를 사용하여 클래스를 명시적으로 로드합니다.
Class.forName("Coffee");
System.out.println("Class loaded successfully.");
// 객체를 생성하고 메소드 실행
Coffee obj = new Coffee();
obj.drink();
}
}
Output
Main method started.
Coffee class is loaded by the JVM!
Class loaded successfully.
Method of Coffee class is executed
* 참고 : 로드된 클래스 파일 하나 당 해당 클래스의 객체가 하나만 생성됩니다.
- Linking(연결) : 로드된 클래스를 실행 준비 상태로 만드는 역할을 합니다. 이 단계는 3단계로 구성됩니다.
1. Verify(검증) : 바이트 코드가 JVM 규칙을 준수하고 실행하기에 안전한지 확인합니다.
2. Prepare(준비) : 정적 변수에 대한 메모리를 할당하고 기본값을 지정합니다.
3. Resolve(해결) : 기호 참조를 메모리의 직접 참조로 변환합니다.
- Initialization(초기화)
1. 정적 변수에 실제 값을 할당합니다.
2. 클래스에 정의된 정적 블록을 실행합니다.
2. 메모리 영역 (Runtime Data Areas)
JVM의 메모리 영역은 아래 5가지로 구분됩니다.
- 메서드 영역 : 클래스 명, 부모 클래스, 메서드, 변수 및 정적 데이터와 같은 클래스 수준 정보를 저장합니다. JVM 전체에서 공유됩니다.
- 힙 영역 : 모든 객체를 저장합니다. JVM 전체에서 공유됩니다.
- 스택 영역 : 각 스레드는 자체 런타임 스택을 가지머, 메서드 호출과 지역 변수를 스택 프레임에 저장합니다. 스레드가 종료되면 스택은 소멸됩니다.
- PC 레지스터 : 각 스레드에서 현재 실행 중인 명령어의 주소를 저장합니다.
- 네이티브 메서드 스택 : 각 스레드는 네이티브 메서드 실행을 위한 별도의 스택을 갖습니다.
3. 실행 엔진(Execution Engine)
실행 엔진은 .class(바이트코드)를 실행하는 역할을 합니다. 바이트코드를 한 줄씩 읽고, 다양한 메모리 영역에 있는 데이터와 정보를 사용하여 명령어를 실행합니다.
단순히 코드를 읽는 것을 넘어, 프로그램이 돌아가는 동안 힙(Heap) 영역에 객체를 생성하고, 메서드가 호출될 때마다 스택(Stack) 영역에 지역 변수를 할당하며 실질적인 메모리 운용을 담당합니다.
실행 엔진은 크게 세 가지 핵심 구성 요소로 나뉩니다.
- 인터프리터 : 바이트코드를 한 줄씩 해석한 후 실행합니다. 하지만 하나의 메서드가 여러 번 호출될 경우, 매번 해석이 필요하다는 단점이 있습니다.
- JIT(Just-In-Time Compiler) : 인터프리터의 효율성을 높아기 위해 사용됩니다. 전체 바이트코드를 컴파일하여 네이티브 코드로 변환하므로, 인터프리터가 반복되는 메서드 호출을 만날 때마다 JIT는 해당 부분에 대해 네이티브 코드를 직접 제공합니다. 따라서 재해석이 필요하지 않아 효율성이 향상됩니다.
- 가비지 컬렉터(GC) : 실행 엔진이 코드를 실행하며 힙(Heap) 영역에 생성했던 객체들 중, 더 이상 참조되지 않는(쓰지 않는) 객체들을 찾아내어 삭제합니다.
* 네이티브 코드 : CPU가 직접 이해할 수 있는 기계어
정리하자면,
우리가 작성한 .java 파일을 컴파일(빌드)하면, JDK에 포함된 자바 컴파일러(javac)가 이를 읽어 컴퓨터(JVM)가 이해할 수 있는 .class 파일(바이트코드)을 생성합니다.이후 JVM의 클래스 로더가 이 클래스 파일을 읽어 메서드 영역에 클래스 정보를 저장하며 메모리로 로드합니다.
프로그램이 실행됨에 따라 실행 엔진은 객체를 힙 영역에 생성하고, 메서드 호출과 지역 변수를 스택 영역에 할당하며 관리합니다.
JVM의 실행 엔진이 바이트코드를 해석하거나 JIT 컴파일을 통해 기계어로 변환하며 실제로 코드를 실행하게 되며 이 과정을 통해 자바 애플리케이션이 동작하게 됩니다.
학습용으로 작성한 포스팅이므로 오류가 있을 수 있습니다.
잘못된 내용이나 보완이 필요한 부분이 있다면 댓글로 알려주시면 감사하겠습니다.
궁금한 점도 편하게 남겨주세요.
출처
- JVM, JRE, and JDK - Fully Explained in 5 Minutes
https://youtu.be/KctLuhwFEQ8?si=ijA_c9dLYRT_HmQD
- Difference between JDK, JRE and JVM in Java
https://howtodoinjava.com/java/basics/jdk-jre-jvm/
- How JVM Works - JVM Architecture
https://www.geeksforgeeks.org/java/how-jvm-works-jvm-architecture/
'JAVA' 카테고리의 다른 글
| 서블릿(Servlet)이란? (1) | 2026.04.09 |
|---|---|
| [UML] 클래스 다이어그램 (0) | 2024.07.14 |
| 객체 지향 프로그래밍 4요소 - 다형성(Polymorphism) (0) | 2024.06.30 |
| 객체와 클래스 (0) | 2024.06.23 |
| 자바의 변수들(일반 변수, 참조 변수) (0) | 2024.06.23 |