[Java] JVM 구조 정리

simhani1·2025년 5월 6일

Java

목록 보기
2/3
post-thumbnail

JVM 구조

JVM은 Java Virtual Machine의 약자로 Java 바이트 코드를 운영체제와 무관하게 실행할 수 있게 해주는 가상 머신입니다. 즉 Java 프로그램이 다양한 플랫폼(Windows, Linux, Mac...) 어디서나 실행되도록 만들어주는 핵심 역할을 합니다.

JVM은 아래와 같이 구성되어 있습니다.

  • 클래스 로더(Class Loader)
  • 실행 엔진(Execution Engine)
    • 인터프리터
    • JIT 컴파일러
    • Garbage Collector(GC)
  • 런타임 데이터 영역(Runtime Data Area)
    • Method 영역
    • Heap 영역
    • PC Register
    • Stack 영역
    • Native Method Stack 영역
  • JNI(Java Native Interface)
  • 네이비트 메서드 라이브러리(Native Method Libraries)

1. 클래스 로더(Class Loader)

JVM 메모리 내에 *.class를 로드하여 실행 준비를 합니다. 클래스 로더 시스템이 끝나면 Class 객체를 생성하여 Heap 영역에 저장합니다.

1. Loading(로드) : 클래스 파일을 JVM의 메서드 영역에 올리는 단계입니다.
2. Linking(링크) : 클래스가 실행 가능한 형태로 준비되는 단계입니다.

  • Verification(검증) : JVM 명세 위반 여부를 검사하며 클래스 파일의 유효성을 검증합니다.
  • Preparation(준비) : 메모리를 할당합니다. 이때 static 변수들을 기본값으로 메모리에 할당합니다.
  • Resolution(분석) : 클래스 파일 안의 상수 풀(Constant Pool)에 저장되어 있는 심볼릭 레퍼런스(symbolic reference)를 직접 참조(direct reference)로 변경합니다.

3. Initialization(초기화) : 클래스 변수들이 코드에서 지정한 값으로 초기화되며, 정적 초기화 블록도 함께 실행됩니다. 부모 클래스부터 자식 클래스까지 차례대로 초기화됩니다.


2. 실행 엔진(Execution Engine)

클래스 로더를 통해 메모리에 적재된 .class 를 배치한 후, 해당 코드들을 실행하는 역할을 합니다. 운영체제는 byte code(*.class)를 이해할 수 없으므로 JVM이 이를 실제 명령으로 변환해 CPU가 실행할 수 있도록 native code로 변환합니다. 그 과정에서 인터프리터와 JIT 컴파일러가 사용됩니다.

인터프리터

바이트 코드를 한 줄씩 해석하며 실행합니다. 중복되는 byte code에 대해서도 매번 컴파일하게 되어 비효율적이고 실행 시간이 오래 걸릴 수 있습니다.

JIT 컴파일러(Just-In-Time Compiler)

인터프리터만 사용할 경우, 메서드가 반복적으로 호출될 때마다 매번 바이트코드를 해석해야 합니다. 이는 불필요한 반복 작업이고 JVM은 이를 효율적으로 처리하고자 JIT 컴파일러를 도입했습니다.

먼저 전체 byte code를 native code로 컴파일하고 캐싱합니다. 이후 메서드가 반복 호출될 경우, 이미 컴파일된 native code를 실행합니다. 따라서 인터프리팅하여 실행하는 것보다 더 빠르게 진행될 수 있습니다.

하지만 인터프리터가 코드를 해석하는 것보다 컴파일 시간은 더 오래 걸립니다. 그래서 한 번만 실행되는 코드라면 인터프리터를 사용하는 것이 더 나은 방법일 수 있습니다. 또한 캐시를 사용한다는 점에서 자원이 더 많이 들기도 합니다.

이런 상황을 고려하여 JIT 컴파일러는 메서드의 호출 빈도를 내부적으로 추적하고, 호출 횟수가 기준치를 넘을 때에만 JIT 컴파일 방식으로 실행합니다.

GC(Garbage Collector)

Heap 메모리에서 더는 사용되지 않는 객체를 자동으로 탐지하고 제거하는 역할을 수행합니다. 이를 통해 불필요한 메모리 점유를 줄이고 OutOfMemory 발생 가능성을 낮춰줍니다. 따라서 개발자는 메모리 관리에 신경 쓰지 않고 개발에 집중할 수 있습니다.


3. 런타임 데이터 영역(Runtime Data Area)

Method 영역(Java 8 이후 Metaspace)

모든 스레드가 공유하며 클래스의 메타데이터(필드, 메서드, 접근 제어자 등), static 변수, 상수 풀 등을 저장합니다.

Runtime Constant Pool

  • 메서드 영역에 존재하는 별도의 관리 영역
  • 인스턴스마다 별도의 Constant Pool 테이블이 존재하고, 클래스 생성 시점에 참조해야 하는 정보를 상수로 저장
  • 리터럴 상수, 심볼릭 레퍼런스, 메서드 및 필드 참조, 타입 정보를 저장
  • 이곳에서 JVM은 심볼릭 참조를 찾아 실제 메모리 주소로 Resolution(해석) 진행
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);  // 결과는 true

Heap 영역

모든 스레드가 공유하며 객체 인스턴스를 저장합니다. Heap의 참조 주소는 Stack이 갖고 있습니다. 따라서 Stack의 변수나 필드에서 참조됩니다. 만약 Stack에서 참조하는 변수나 필드가 없다면 의미 없는 객체이므로 JVM은 GC를 실행시켜 메모리를 최적화합니다.

Stack 영역

프로그램 실행 과정에서 임시로 할당되었다가 메서드가 종료되면 바로 소멸하는 데이터를 저장합니다. 예를 들어 호출된 메서드의 매개 변수, 지역 변수, 반환값, 연산 과정에서 나온 값 등이 있습니다. 그리고 메서드가 호출될 때마다 각 메서드를 위한 스택 프레임을 생성하고 메서드가 종료될 때 해당 프레임을 삭제합니다.

Stack에는 Primitive Type 값, Heap에는 Reference Type 값이 저장됩니다.

class Person {
    private int id;
    private String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

public class Main {
    public static void main(String[] args) {
        int id = 1;
        String name = "simhani1";
        Person person = null;
        person = buildPerson(id, name);
    }

    private static Person buildPerson(int id, String name) {
        return new Person(id, name);
    }
}

PC Register

스레드마다 현재 실행 중인 byte code 명령어의 주소를 저장합니다.

Native Method Stack 영역

JVM의 실행 엔진에서 필요한 C/C++ 기반의 네이티브 라이브러리 모음입니다. JNI를 통해 접근 가능하며, 운영체제 또는 하드웨어와의 저수준 기능을 수행하는 데 사용됩니다.


4. JNI(Java Native Interface)

JNI는 Native Method Library와 상호작용하기 위해 사용되는 인터페이스입니다. 이 인터페이스를 통해 JVM이 Native Method를 수행할 수 있습니다.

참고
JVM 내부 구조 & 메모리 영역 총정리
JVM에 관하여 - Part 3
How Many Types of Memory Areas are Allocated by JVM?

0개의 댓글