☕️ JVM이란 무엇인가?
JVM(Java Virtual Machine)은 자바 바이트코드를 실행하는 가상 머신입니다. 자바로 작성된 프로그램이 플랫폼(운영체제)에 독립적으로 실행될 수 있도록 중간 매개체 역할을 합니다.
개발자는 한 번 코드를 작성하면, JVM이 각 운영체제에 맞게 해석하여 실행해주기 때문에 플랫폼에 관계없이 동일한 코드를 실행할 수 있습니다.
JVM의 등장 배경
플랫폼 의존성 문제
1990년대 초반, 제임스 고슬링(James Gosling)과 그의 팀은 당시 프로그래밍 세계가 직면한 중요한 문제를 해결하고자 했습니다. 그 문제는 바로 플랫폼 의존성이었습니다.
Windows, Unix, Mac 등 서로 다른 운영체제에서 프로그램을 실행하기 위해 각 플랫폼마다 코드를 새로 작성하거나 수정해야 했으며, 심지어 같은 코드라도 각 플랫폼별로 별도의 컴파일 과정과 테스트를 거쳐야 했습니다. 이는 개발 시간을 크게 증가시키고, 유지보수를 어렵게 만들었으며, 소프트웨어의 일관성을 유지하기도 힘들었습니다.
기존 컴파일러 언어의 한계
특히 C/C++과 같은 컴파일러 기반 언어들은 코드를 특정 플랫폼의 기계어로 직접 컴파일했기 때문에, 다른 플랫폼에서 실행하려면 해당 플랫폼에 맞게 소스 코드를 수정하고 다시 컴파일해야 했습니다. 이런 과정은 시간이 많이 소요되고 오류가 발생하기 쉬웠습니다.
"Write Once, Run Anywhere"
이러한 배경에서 "한 번 작성하고 어디서나 실행할 수 있는(Write Once, Run Anywhere)" 언어에 대한 필요성이 대두되었고, 이를 실현하기 위해 JVM이 탄생했습니다.
자바는 컴파일러 언어의 성능적 이점을 유지하면서도 인터프리터 언어의 플랫폼 독립성을 가질 수 있는 혁신적인 접근 방식을 채택했습니다.
JVM의 컴파일 방식
- Java 소스 코드(.java 파일)는 Java 컴파일러(javac)에 의해 바이트코드(.class 파일)로 컴파일됩니다.
- 각 플랫폼(Windows, Linux, macOS 등)에 설치된 JVM이 이 바이트코드를 해석하고 실행합니다.
- JVM은 바이트코드를 실행할 때 인터프리터 방식으로 한 줄씩 해석하여 실행하거나,
- Just-In-Time(JIT) 컴파일러를 사용하여 자주 실행되는 코드를 네이티브 코드로 변환하여 성능을 최적화합니다.
소스 코드 → 바이트코드 → 기계어
이 두 단계 컴파일 과정을 통해 자바는 다음과 같은 이점을 얻었습니다.
- 컴파일 언어의 타입 검사와 최적화 혜택
- 인터프리터 언어의 플랫폼 독립성
- 개발자가 플랫폼별 차이점을 신경 쓰지 않고 한 번만 코드를 작성하면 되는 편리
자바 프로그램 실행 과정
-
소스 코드 작성 (.java 파일)
-
자바 컴파일러(javac) 실행 → .class 파일 생성 (바이트코드)
-
JVM이 .class 파일을 로드 및 검증
-
바이트코드를 실행 엔진에서 해석하여 기계어로 변환
-
애플리케이션 실행
JVM의 주요 구성 요소
JVM은 크게 세 가지 주요 부분으로 구성됩니다.

- 클래스 로더 시스템(Class Loader System)
- 자바 바이트코드를 JVM으로 로드하는 역할
- 계층적 구조(부트스트랩, 확장, 애플리케이션 클래스 로더)
- 로딩, 링크, 초기화 단계를 통해 클래스를 메모리에 탑재
- 실행 엔진(Execution Engine)
- 로드된 바이트코드를 기계어로 변환하고 실행
- 인터프리터: 바이트코드를 한 줄씩 해석하고 실행
- JIT 컴파일러: 반복 실행되는 코드를 기계어로 컴파일하여 성능 향상
- 가비지 컬렉터: 더 이상 사용되지 않는 메모리를 자동으로 정리
- 런타임 데이터 영역(Runtime Data Areas)
- JVM이 프로그램을 실행하기 위해 OS로부터 할당받는 메모리 영역
- 메서드 영역, 힙, 스택, PC 레지스터, 네이티브 메서드 스택으로 구성
클래스 로더 시스템
클래스 로더는 JVM이 실행 중에 필요한 클래스를 동적으로 로드하는 중요한 역할을 합니다. 클래스 로딩은 다음 세 단계로 진행됩니다.
로딩(Loading)
- 클래스 파일을 찾아 바이트코드를 읽고 메모리에 로드
- 클래스의 기본 정보(패키지, 부모 클래스 등)를 메서드 영역에 저장
- 클래스를 나타내는 Class 객체를 생성
링크(Linking)
- 검증(Verification): 바이트코드가 JVM 명세에 맞게 올바르게 작성되었는지 확인
- 준비(Preparation): 클래스 변수(static 변수)를 위한 메모리 할당 및 기본값으로 초기화
- 해결(Resolution): 심볼릭 참조를 직접 참조로 변환
초기화(Initialization)
- 클래스 변수를 적절한 값으로 초기화(static 초기화 블록 실행)
- 부모 클래스가 초기화되지 않았다면 먼저 부모 클래스 초기화
클래스 로더의 종류
JVM은 계층적인 클래스 로더 구조를 가지고 있습니다:
- 부트스트랩 클래스 로더(Bootstrap Class Loader)
- JVM의 핵심 클래스를 로드(java.lang.* 패키지 등)
- 네이티브 코드로 구현되며 자바 코드에서 직접 참조 불가능
- 확장 클래스 로더(Extension Class Loader) / 플랫폼 클래스 로더
- JDK 확장 디렉토리의 클래스 로드
- 자바 9부터 플랫폼 클래스 로더로 이름 변경
- 애플리케이션 클래스 로더(Application Class Loader) / 시스템 클래스 로더
- 클래스패스 상의 애플리케이션 클래스 로드
- 사용자가 정의한 클래스를 로드하는 기본 클래스 로더
- 사용자 정의 클래스 로더(User-Defined Class Loader)
- 개발자가 직접 구현한 클래스 로더
- 특별한 로딩 메커니즘이 필요할 때 사용
클래스 로더 위임 모델
클래스 로더는 '위임 모델(Delegation Model)'에 따라 작동합니다.
- 클래스 로딩 요청을 받으면, 먼저 부모 클래스 로더에게 위임
- 부모 클래스 로더가 클래스를 찾지 못하면, 자식 클래스 로더가 직접 클래스를 로드 시도
- 모든 클래스 로더가 클래스를 찾지 못하면 ClassNotFoundException 발생
ClassLoader loader = MyClass.class.getClassLoader();
System.out.println(loader);
ClassLoader parent = loader.getParent();
System.out.println(parent);
ClassLoader bootstrap = parent.getParent();
System.out.println(bootstrap);
가비지 컬렉션(GC)
가비지 컬렉션은 JVM에서 더 이상 사용되지 않는 객체의 메모리를 자동으로 회수하는 프로세스입니다. 이는 개발자가 명시적으로 메모리 관리를 하지 않아도 되게 하여 메모리 누수와 같은 문제를 줄여줍니다.
가비지 컬렉션의 기본 원리
- 객체 도달성(Reachability): GC는 더 이상 도달할 수 없는(unreachable) 객체를 식별
- GC 루트(GC Roots): 항상 도달 가능한 객체들(스레드, 정적 필드, JNI 참조 등)
- 마크 앤 스윕(Mark and Sweep): 도달 가능한 객체를 마크하고, 마크되지 않은 객체를 제거
세대별 가비지 컬렉션
대부분의 JVM은 '세대별 가설(Generational Hypothesis)'에 기반하여 힙을 여러 세대로 나누어 관리합니다:
- Young Generation(새로운 세대)
- Eden Space: 새로 생성된 객체가 할당되는 공간
- Survivor Spaces(S0, S1): Minor GC 이후 살아남은 객체가 이동하는 공간
- Minor GC: Young Generation에서 발생하는, 상대적으로 빠른 가비지 컬렉션
- Old Generation(오래된 세대)
- Young Generation에서 일정 기간 살아남은 객체가 이동
- Major GC / Full GC: Old Generation에서 발생하는, 상대적으로 오래 걸리는 가비지 컬렉션
JIT 컴파일러
Just-In-Time(JIT) 컴파일러는 JVM 성능의 핵심 요소로, 인터프리터의 단점을 보완하여, 반복 실행되는 코드를 기계어로 변환해 성능을 향상시킵니다.
JIT 컴파일러의 작동 원리
- 인터프리터 사용 초기 실행: 처음에 JVM은 인터프리터를 사용하여 바이트코드 실행
- 핫스팟 감지: JVM이 자주 실행되는 코드(핫스팟)를 모니터링
- 컴파일 결정: 특정 메서드나 루프가 '핫'하다고 판단되면 JIT 컴파일 대상으로 선정
- 최적화 컴파일: 선정된 코드를 네이티브 코드로 컴파일하고 다양한 최적화 적용
- 컴파일 코드 캐싱: 컴파일된 코드를 코드 캐시에 저장하여 재사용
JIT 컴파일러의 최적화 기법
- 인라인화(Inlining): 메서드 호출을 메서드 본문으로 대체하여 호출 오버헤드 제거
- 루프 최적화: 루프 언롤링, 루프 불변 코드 이동 등
- 탈출 분석(Escape Analysis): 객체가 메서드 범위를 벗어나지 않으면 힙 할당 제거
- 죽은 코드 제거(Dead Code Elimination): 실행되지 않는 코드 제거
- 락 생략(Lock Elision): 스레드 안전성을 해치지 않는 선에서 락 제거
- 타입 특화(Type Specialization): 런타임 타입 정보를 활용한 최적화
컴파일 수준
HotSpot JVM은 두 가지 컴파일 모드를 제공합니다:
- C1 컴파일러(Client Compiler)
- 빠른 시작 시간과 기본적인 최적화에 중점
- 작은 애플리케이션이나 클라이언트 애플리케이션에 적합
- C2 컴파일러(Server Compiler)
- 더 공격적인 최적화와 장기 실행 성능에 중점
- 서버 애플리케이션에 적합
- Tiered Compilation
- C1과 C2를 함께 사용하는 방식
- 처음에는 C1으로 빠르게 컴파일한 후, 핫스팟으로 확인된 코드를 C2로 재컴파일
- 대부분의 현대 JVM에서 기본 설정
Runtime Data Areas

JVM은 자바 애플리케이션을 실행하기 위해 다음과 같은 메모리 영역을 사용합니다.
1. 메서드 영역(Method Area = Class Area = Static Area)
- 클래스 구조(필드, 메서드 데이터), 상수, 정적 변수, 메서드 코드 등을 저장
- JVM당 하나만 생성되며 모든 스레드가 공유
- JDK 8부터는 PermGen이 제거되고 Metaspace로 대체(네이티브 메모리 사용)
public static final String CONSTANT = "This is stored in Method Area";
public static int counter = 0;
2. 힙 영역(Heap)
- 객체와 배열이 저장되는 동적 메모리 영역
- 모든 스레드가 공유하는 영역으로 GC의 주요 대상
- JVM 실행 시 생성되고 종료 시 소멸
- 세대 기반 구조(Young Generation, Old Generation)로 나뉘어 관리
Person person = new Person("John");
int[] numbers = new int[10];
3. 스택 영역(Stack Area)
- 스레드마다 별도로 생성되는 영역
- 메서드 호출 시 생성되는 프레임을 저장
- 지역 변수, 매개변수, 반환 값, 연산 중간 결과 등을 임시 저장
- 메서드가 종료되면 해당 스택 프레임이 제거됨
public void calculateSum() {
int a = 10;
int b = 20;
int sum = a + b;
}
4. PC 레지스터 (Program Counter Register)
- 각 스레드마다 생성되며, 현재 실행 중인 명령의 주소를 저장
- JVM 명령의 실행 순서를 제어
5. 네이티브 메서드 스택(Native Method Stack)
- 네이티브 코드(C/C++ 등으로 작성된 코드)를 위한 스택
- JNI(Java Native Interface)를 통해 호출되는 네이티브 메서드의 매개변수와 지역 변수를 저장
네이티브 인터페이스(Java Native Interface, JNI)
JVM은 기본적으로 바이트코드를 실행하는 환경이지만, 외부의 네이티브 코드와 연결할 필요가 있을 때 JNI를 사용합니다.
이를 통해 C, C++ 등으로 작성된 라이브러리를 실행하거나, 자바에서 접근할 수 없는 하드웨어나 운영 체제의 기능에 접근할 수 있습니다.
JDK, JRE 및 JVM 이해하기
Java 환경에서 자주 만나게 되는 세 가지 핵심 용어인 JDK, JRE, JVM은 서로 밀접하게 연관되어 있지만 각각 다른 역할을 담당합니다. 이 세 요소의 관계를 명확히 이해하는 것은 Java 개발 환경을 효과적으로 구성하고 관리하는 데 중요합니다.
JRE (Java Runtime Environment)
JRE는 Java 애플리케이션을 실행하기 위한 환경으로, JVM과 표준 Java API 라이브러리를 포함합니다.
구성 요소
- JVM: Java 바이트코드 실행 엔진
- Java 클래스 라이브러리: 표준 클래스 라이브러리(java.lang, java.util 등)
- 브리징 소프트웨어: Java 애플리케이션과 기본 OS 간의 인터페이스
용도
- Java 애플리케이션 실행만 필요한 경우(개발이 아닌 경우) 설치
- 개발된 Java 프로그램을 배포할 때, 최종 사용자는 JRE만 있으면 충분
JDK (Java Development Kit)
JDK는 Java 애플리케이션을 개발하기 위한 포괄적인 도구 모음으로, JRE를 포함하며 개발에 필요한 추가 도구를 제공합니다.
구성 요소
- JRE: Java 애플리케이션 실행 환경(JVM 포함)
- 개발 도구:
- javac: Java 컴파일러
- javadoc: API 문서 생성기
- jar: 아카이브 생성 유틸리티
- jdb: 디버거
- 기타 다양한 개발 유틸리티
- 추가 라이브러리: 개발용 클래스 라이브러리 및 헤더 파일
용도
- Java 애플리케이션 개발에 필수
- 전문 개발자, 학생, Java 코드를 작성하는 모든 사람에게 필요
세 구성 요소의 관계
따라서 세 구성 요소의 관계는 포함 관계로 이해할 수 있습니다.
JDK > JRE > JVM
- JDK는 가장 포괄적인 패키지로, JRE와 개발 도구를 모두 포함합니다.
- JRE는 JVM과 표준 라이브러리를 포함하며, 애플리케이션 실행 환경을 제공합니다.
- JVM은 가장 핵심적인 구성 요소로, 바이트코드를 실행하는 엔진입니다.