JVM 내부 구조 완벽 이해하기

FIRE-918·2026년 4월 4일
post-thumbnail

글을 시작하며

최근 코틀린 위주로 개발하다 보니 Java 지식이 점점 희미해지고 있었습니다. 문법만 다시 익히는 것으론 부족하다는 생각이 들었고, Java를 제대로 이해하려면 내부 구조부터 파악해야 한다고 판단했습니다. 이 글에서는 JVM의 내부 구조를 중심으로 Java 프로그램이 어떻게 실행되는지 정리해보겠습니다.


JDK(Java Development Kit)의 구성 요소

JDK(Java Development Kit)는 자바 개발에 필요한 전체 도구 모음입니다. JRE와 javac 같은 개발 도구를 포함하고 있으며, 구성 요소 간의 포함 관계는 다음과 같습니다.

JDK: 자바 개발 도구 모음
JRE: JVM + 자바 표준 라이브러리
JVM: 바이트코드 실행 가상 머신
javac: .java → .class 바이트코드 변환


JVM(Java Virtual Machine)

JVM은 자바 바이트코드를 실행하는 가상 머신입니다. 운영체제 위에서 동작하며, Java 프로그램이 어떤 OS에서도 실행될 수 있도록 플랫폼 독립성을 제공합니다.

주요 역할

  • 바이트코드 (.class) 실행
  • 메모리 관리 (Heap, Stack 등)
  • 가비지 컬렉션(GC)
  • 인터프리터 & JIT 컴파일 실행

JVM이 없는 언어와 비교

C, C++ 같이 JVM이 없는 언어는 특정 OS에 맞게 빌드된 실행 파일을 생성합니다. 따라서 Mac에서 빌드한 바이너리는 Linux에서 실행되지 않으며, OS마다 별도로 빌드해야 합니다.

반면 Java는 JVM이 중간에서 OS의 차이를 흡수하기 때문에, 한 번 컴파일한 .class 파일을 어떤 환경에서든 동일하게 실행할 수 있습니다.


빌드(Build)와 컴파일(Compile)

두 개념은 비슷해 보이지만 범위가 다릅니다.

  • 컴파일(Compile): 소스코드 (.java) 를 JVM이 이해할 수 있는 바이트코드 (.class) 로 변환하는 작업
  • 빌드(Build): 컴파일 + 테스트 + 라이브러리 포함 + 패키징까지, 실행 가능한 결과물 (.jar) 을 만드는 전체 과정

즉, 컴파일은 빌드 과정의 일부입니다.

# 컴파일
javac Hello.java   → Hello.class

# 빌드
./gradlew build   → app.jar

생성된 app.jar는 .class 파일과 라이브러리, 리소스를 하나로 묶은 배포용 패키지입니다. JVM만 설치되어 있다면 OS에 관계없이 아래 명령 하나로 실행할 수 있습니다.

java -jar app.jar

JVM 내부 흐름

이제부터가 이 글의 핵심입니다. JVM이 바이트코드를 어떻게 실행하고 메모리를 관리하는지, 그 내부 흐름을 단계별로 살펴보겠습니다.

.class (바이트코드)
   ↓
1. Class Loader        → 클래스 정보를 JVM에 적재
   ↓
2. Runtime Data Area   → 메모리에 데이터 저장
   ↓
3. Execution Engine    → 바이트코드 실행
   ├─ 인터프리터
   └─ JIT 컴파일러
   ↓
4. GC (필요 시)        → 불필요한 메모리 정리

1. Class Loader

.class 파일을 JVM 메모리에 올리는 역할을 합니다. 프로그램 실행 전에 클래스 정보를 JVM에 적재하며, 필요한 시점에 동적으로 로딩합니다.

2.Runtime Data Area (JVM 메모리)

로드된 클래스와 객체가 실제로 저장되는 공간입니다. 크게 세 영역으로 나뉩니다.

Heap (힙): 객체와 인스턴스 변수가 저장됩니다. 모든 스레드가 공유하며, GC가 주기적으로 불필요한 메모리를 정리합니다.

User user = new User(); // user 객체는 Heap에 저장

Stack (스택): 메서드 실행 시 생성되는 공간으로, 지역 변수가 저장됩니다. 메서드가 종료되면 자동으로 제거되며, 스레드마다 독립적으로 생성됩니다.

void test() {
    int a = 10; // a는 Stack에 저장
}

Method Area: 클래스 구조, static 변수, 메서드 코드 등 클래스 수준의 정보가 저장됩니다. 모든 스레드가 공유합니다.

class User {
    static int count = 0; // count는 Method Area에 저장
}

3.Execution Engine (실행 엔진)

바이트코드를 실제 기계어로 변환해 실행하는 핵심 엔진입니다. 인터프리터와 JIT 컴파일러 두 가지 방식을 조합해 사용합니다.

javac가 문법 검사와 바이트코드 변환을 담당한다면, Execution Engine은 그 바이트코드를 빠르게 실행하는 것이 목적입니다.

인터프리터: 바이트코드를 한 줄씩 읽어 실행합니다. 시작이 빠르지만, 같은 코드를 반복 실행할 때 매번 해석해야 하므로 속도가 느립니다.

JIT 컴파일러: 자주 실행되는 코드(Hot Spot)를 감지해 기계어로 미리 컴파일합니다. 이후 동일한 코드가 반복될 때는 컴파일된 기계어를 바로 실행하므로 성능이 크게 향상됩니다.

JVM은 이 두 가지를 혼합해 사용합니다. 처음엔 인터프리터로 빠르게 시작하고, 반복 실행이 많은 구간은 JIT로 최적화하는 방식입니다.

4.GC (Garbage Collector)

더 이상 참조되지 않는 Heap 메모리를 자동으로 정리합니다. 개발자가 직접 메모리를 해제할 필요 없이, GC가 주기적으로 동작하며 메모리 누수를 방지하고 안정적인 실행 환경을 유지합니다.


마치며

JVM의 내부 흐름을 정리하면 다음과 같습니다.

.class → Class Loader → Runtime Data Area → Execution Engine(인터프리터 + JIT) → GC

JVM이라는 단어는 Java를 처음 배울 때부터 익숙하게 들어왔지만, 막상 내부를 들여다보니 그동안 표면만 알고 있었다는 걸 깨달았습니다. Class Loader부터 GC까지, 각각의 개념이 따로 노는 것이 아니라 하나의 흐름 안에서 맞물려 돌아간다는 것을 이번 정리를 통해 이해할 수 있었습니다.

코틀린을 쓰더라도 결국 JVM 위에서 동작한다는 사실은 변하지 않는 만큼, 이번 정리가 앞으로의 개발에도 단단한 기반이 되어줄 것 같습니다.

profile
학습한 내용을 기록하고 공유하며 함께 성장하는 백엔드 개발자입니다.

0개의 댓글