JVM 구조와 동작 원리 - Java 개발자라면 반드시 알아야 할 핵심 개념

들어가며

Java 개발자로서 JVM(Java Virtual Machine)을 이해하는 것은 단순히 “알면 좋은” 수준이 아니라 필수적인 역량입니다. JVM의 동작 원리를 이해하면 메모리 누수 문제를 해결하고, 애플리케이션 성능을 최적화하며, 기술 면접에서도 깊이 있는 답변을 할 수 있게 됩니다. 이 글에서는 JVM의 구조와 동작 원리를 실무 관점에서 상세하게 살펴보겠습니다.

JVM이란 무엇인가?

JVM은 Java Virtual Machine의 약자로, 자바 바이트코드를 실행할 수 있는 가상 머신입니다. 여기서 ‘가상 머신’이란 물리적인 컴퓨터가 아닌 소프트웨어로 구현된 컴퓨터를 의미합니다. JVM은 자바 프로그램과 운영체제 사이에서 중개자 역할을 수행하며, 이로 인해 자바는 “Write Once, Run Anywhere”라는 플랫폼 독립성을 갖게 됩니다.

자바 프로그램이 실행되는 과정을 간단히 살펴보면, 먼저 개발자가 작성한 .java 소스 파일이 자바 컴파일러(javac)에 의해 .class 바이트코드 파일로 변환됩니다. 이 바이트코드는 특정 운영체제나 하드웨어에 종속되지 않은 중간 언어입니다. 그 다음 JVM이 이 바이트코드를 읽어서 해당 운영체제가 이해할 수 있는 기계어로 변환하여 실행합니다.

이러한 구조 덕분에 Windows에서 컴파일한 자바 프로그램을 Linux나 macOS에서도 별도의 수정 없이 실행할 수 있습니다. 물론 각 운영체제별로 JVM 구현체는 다르지만, 바이트코드 스펙은 동일하기 때문에 “한 번 작성하면 어디서든 실행된다”는 자바의 철학이 실현되는 것입니다.

image

JVM의 핵심 특징

JVM은 몇 가지 중요한 특징을 가지고 있습니다.

첫째, 플랫폼 독립성입니다. JVM이 운영체제와 자바 프로그램 사이의 추상화 계층 역할을 하기 때문에, 동일한 바이트코드가 어떤 운영체제에서든 실행될 수 있습니다.

둘째, 자동 메모리 관리입니다. 자바에서는 JVM의 가비지 컬렉터(Garbage Collector) 가 더 이상 사용되지 않는 객체를 자동으로 탐지하고 메모리에서 제거합니다. 이는 개발 생산성을 높이고 메모리 관련 버그를 줄이는 데 큰 도움이 됩니다.

셋째, JVM은 스택 기반 가상 머신입니다. 대부분의 연산이 스택을 통해 이루어지며, 이는 JVM이 하드웨어와 독립적으로 동작할 수 있도록 설계된 이유 중 하나입니다.


JVM 옵션(Runtime Flags)으로 보는 내부 동작 제어

JVM의 구조를 이해했다면, 다음 단계는 JVM 옵션을 통해 내부 동작을 직접 제어하는 것입니다.
실무에서는 JVM 옵션 하나로 장애가 발생하기도, 해결되기도 합니다.

힙 관련 옵션

  • -Xms : 힙의 초기 크기
  • -Xmx : 힙의 최대 크기

힙 크기가 너무 작으면 GC가 빈번하게 발생해 성능이 저하되고,
너무 크면 GC 한 번에 애플리케이션이 멈추는 시간이 길어질 수 있습니다.

1
-Xms2g -Xmx2g

초기 크기와 최대 크기를 동일하게 설정하면 불필요한 힙 확장 비용을 줄일 수 있습니다.

스택 관련 옵션

  • Xss : 스레드 스택 크기
    1
    
    -Xss1m
    

    재귀 호출이 많은 애플리케이션에서 StackOverflowError가 발생한다면 스택 크기 조정이 해결책이 될 수 있습니다.

Metaspace 관련 옵션

  • -XX:MaxMetaspaceSize
1
-XX:MaxMetaspaceSize=256m

Java 8 이후 Metaspace는 네이티브 메모리를 사용하므로 제한 없이 증가하다가 서버 전체 메모리를 고갈시킬 수도 있습니다.

장애 분석에 유용한 옵션

1
2
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/heapdump.hprof

OOM 발생 시 힙 덤프를 남겨두면 원인 분석이 가능한 장애가 됩니다.


JVM 아키텍처 상세 분석

JVM의 아키텍처는 크게 세 가지 핵심 컴포넌트로 구성됩니다.

  • 클래스 로더 서브시스템(Class Loader Subsystem)
  • 런타임 데이터 영역(Runtime Data Area)
  • 실행 엔진(Execution Engine)

image

이제 각 컴포넌트를 자세히 살펴보겠습니다.

클래스 로더 서브시스템 (Class Loader Subsystem)

클래스 로더는 .class 파일을 읽어서 JVM 메모리에 로드하는 역할을 담당합니다. 동작 과정은 Loading → Linking → Initialization 세 단계로 이루어집니다.

Loading (로딩) 단계

로딩 단계에서는 .class 파일의 바이트코드를 읽어서 JVM 메모리에 올리는 작업이 수행됩니다. 자바에는 기본적으로 세 가지 클래스 로더가 계층 구조로 존재합니다.

가장 상위에 있는 것이 Bootstrap ClassLoader입니다. 이 클래스 로더는 JVM이 기동될 때 가장 먼저 생성되며, java.lang.Object, java.lang.String 등 핵심 자바 API를 담고 있는 rt.jar(Java 8 이하) 또는 모듈(Java 9 이상)을 로드합니다. Bootstrap ClassLoader는 C/C++로 구현되어 있기 때문에 자바 코드에서 getClassLoader() 메서드를 호출하면 null이 반환됩니다.

그 다음 계층에 있는 것이 Extension ClassLoader(Java 9 이후에는 Platform ClassLoader)입니다. 이 클래스 로더는 jre/lib/ext 폴더에 있는 확장 클래스들을 로드합니다. 암호화 관련 라이브러리나 보안 관련 확장 기능들이 여기에 해당합니다.

마지막으로 Application ClassLoader(System ClassLoader라고도 함)가 있습니다. 이 클래스 로더는 우리가 작성한 애플리케이션의 클래스들, 즉 클래스패스(classpath)에 지정된 경로에 있는 클래스들을 로드합니다. 대부분의 경우 우리가 다루는 클래스들은 이 Application ClassLoader에 의해 로드됩니다.

image image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ClassLoaderDemo {
    public static void main(String[] args) {
        // Application ClassLoader - 우리가 작성한 클래스 로드
        ClassLoader appLoader = ClassLoaderDemo.class.getClassLoader();
        System.out.println("Application ClassLoader: " + appLoader);
        // 출력: sun.misc.Launcher$AppClassLoader@...
 
        // Extension ClassLoader
        ClassLoader extLoader = appLoader.getParent();
        System.out.println("Extension ClassLoader: " + extLoader);
        // 출력: sun.misc.Launcher$ExtClassLoader@...
 
        // Bootstrap ClassLoader - null로 표시됨
        ClassLoader bootstrapLoader = extLoader.getParent();
        System.out.println("Bootstrap ClassLoader: " + bootstrapLoader);
        // 출력: null
 
        // String 클래스는 Bootstrap ClassLoader가 로드
        System.out.println("String ClassLoader: " + String.class.getClassLoader());
        // 출력: null
    }
}

클래스 로더는 위임 모델(Delegation Model) 을 따릅니다. 클래스 로딩 요청이 들어오면, 해당 클래스 로더는 먼저 부모 클래스 로더에게 로딩을 위임합니다. 부모가 해당 클래스를 찾지 못하면 그제서야 자신이 직접 로딩을 시도합니다. 이러한 위임 모델 덕분에 핵심 자바 API가 악의적인 코드로 대체되는 것을 방지할 수 있습니다.

이는 보안과 안정성을 확보하기 위한 JVM의 핵심 설계입니다.

[참고] 실무에서 클래스 로더가 문제가 되는 순간
클래스 로더는 평소에는 의식하지 않지만, 다음 상황에서 심각한 장애의 원인이 됩니다.

  • ClassLoader 메모리 누수
    • WAS 재기동 후에도 Metaspace 사용량이 줄지 않는 경우
    • ThreadLocal + ClassLoader 조합
    • 동적 클래스 생성 (프록시, 리플렉션)

이 경우 힙이 아닌 Metaspace OOM이 발생합니다.

1
java.lang.OutOfMemoryError: Metaspace

이는 객체 누수가 아니라 클래스 자체가 GC 대상이 되지 못하는 구조 때문입니다.
따라서 힙은 멀쩡한데 OOM이 난다 → ClassLoader 누수를 가장 먼저 의심해야 합니다.

Linking (링킹) 단계

링킹 단계는 로드된 클래스를 실행할 수 있도록 준비하는 과정으로, Verification(검증), Preparation(준비), Resolution(해석) 의 세 단계로 구성됩니다.

Verification 단계에서는 로드된 바이트코드가 JVM 스펙에 맞는지 검증합니다. 바이트코드가 올바른 형식인지, 보안 위반이 없는지 등을 확인합니다. 만약 검증에 실패하면 java.lang.VerifyError가 발생합니다.

Preparation 단계에서는 클래스의 static 변수들에 대한 메모리가 할당되고, 기본값으로 초기화됩니다. 여기서 중요한 점은 개발자가 지정한 초기값이 아닌 자료형의 기본값(int는 0, boolean은 false, 참조 타입은 null 등)으로 초기화된다는 것입니다. 실제 초기값 할당은 다음 단계인 Initialization에서 이루어집니다.

Resolution 단계에서는 심볼릭 레퍼런스(Symbolic Reference)가 실제 메모리 주소를 가리키는 다이렉트 레퍼런스(Direct Reference)로 변환됩니다. 예를 들어, 바이트코드에서 다른 클래스를 참조할 때 처음에는 문자열 형태의 이름으로 참조하지만, Resolution 단계에서 실제 메모리 주소로 변환됩니다.

Initialization (초기화) 단계

초기화 단계에서는 클래스의 static 변수에 개발자가 지정한 초기값이 할당되고, static 블록이 실행됩니다. 초기화는 클래스가 처음으로 사용될 때 한 번만 수행되며, 이후에는 이미 초기화된 클래스가 재사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
public class InitializationExample {
    // Preparation 단계: staticVar = 0으로 초기화
    // Initialization 단계: staticVar = 100으로 할당
    private static int staticVar = 100;
 
    static {
        // Initialization 단계에서 실행
        System.out.println("Static block executed!");
        staticVar = 200;
    }
}

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

런타임 데이터 영역은 JVM이 프로그램을 실행하면서 사용하는 메모리 공간입니다.
이 영역은 Method Area, Heap, Stack, PC Register, Native Method Stack의 다섯 가지로 구분됩니다.

이 중 Method Area와 Heap은 모든 스레드가 공유하고,
나머지 세 영역은 스레드별로 독립적으로 생성됩니다.


Method Area (메서드 영역)

Method Area는 JVM이 시작될 때 생성되며, 모든 스레드가 공유하는 영역입니다.
이 영역에는 다음과 같은 정보들이 저장됩니다.

  • 클래스 메타데이터 (클래스 이름, 부모 클래스 정보)
  • 메서드 정보
  • 필드 정보
  • static 변수
  • 상수 풀(Constant Pool)
  • 메서드의 바이트코드

Java 8 이전에는 이 영역이 PermGen(Permanent Generation) 으로 불렸으며,
크기가 고정되어 있어 클래스가 많이 로딩되는 환경에서 다음과 같은 오류가 자주 발생했습니다.

1
java.lang.OutOfMemoryError: PermGen space

Java 8부터는 Method Area가 Metaspace로 변경되었습니다. Metaspace는 힙이 아닌 네이티브 메모리(Native Memory) 를 사용하며, 기본적으로 크기 제한이 없습니다.

-XX:MaxMetaspaceSize=256m 옵션을 통해 제한하지 않으면, 클래스 로더 누수로 인해 서버 전체 메모리가 고갈될 수도 있습니다. 이로 인해 PermGen 관련 메모리 문제가 크게 줄어들었습니다.

Heap Area (힙 영역)

Heap은 객체와 배열이 저장되는 영역으로, new 키워드로 생성된 모든 객체가 이곳에 저장됩니다.

Heap은 가비지 컬렉션 효율을 높이기 위해 세대별(Generational) 구조로 설계되어 있습니다.

  • Young Generation
    • Eden
    • Survivor 0 / Survivor 1
  • Old Generation (Tenured)

객체의 생명 주기는 다음과 같습니다.

  1. 객체 생성 → Eden 영역
  2. Eden 가득 참 → Minor GC 발생
  3. 살아남은 객체 → Survivor 영역 이동
  4. 일정 횟수 이상 생존 → Old Generation 승격
1
2
Person person = new Person("Kim", 25); // Heap
int[] numbers = new int[100];          // Heap

힙은 GC의 대상이 되며, 잘못된 객체 참조 구조는 메모리 누수로 이어집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HeapExample {
    public static void main(String[] args) {
        // person 변수(참조)는 Stack에, Person 객체는 Heap에 저장
        Person person = new Person("Kim", 25);
 
        // 배열도 Heap에 저장
        int[] numbers = new int[100];
 
        // String 리터럴은 String Pool(Method Area)에 저장
        // String 객체는 Heap에 저장
        String str1 = "Hello";  // String Pool
        String str2 = new String("Hello");  // Heap
    }
}

Stack Area (스택 영역)

Stack은 각 스레드마다 별도로 생성됩니다. 메서드가 호출될 때마다 스택 프레임(Stack Frame) 이 생성됩니다.

스택 프레임에는 다음 정보들이 포함됩니다.

  • 지역 변수
  • 매개변수
  • 연산 중간 결과
  • 리턴 주소

기본 타입 값은 스택에 직접 저장되고, 참조 타입은 주소 값만 저장되며 실제 객체는 힙에 존재합니다.

1
2
3
public static void recursiveCall() {
    recursiveCall();
}

종료 조건 없는 재귀 호출은 java.lang.StackOverflowError 오류를 발생시킵니다. 이때, 스택 크기는 -Xss1m 옵션으로 조절할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
public class StackOverflowExample {
    public static void main(String[] args) {
        recursiveCall(0);  // StackOverflowError 발생
    }
 
    public static void recursiveCall(int count) {
        System.out.println("Call count: " + count);
        recursiveCall(count + 1);  // 종료 조건 없는 무한 재귀
    }
}

PC Register (Program Counter)

PC Register는 현재 실행 중인 JVM 명령어의 주소를 저장합니다. 각 스레드마다 하나씩 존재하며, JVM이 멀티스레딩을 구현할 수 있는 핵심 요소입니다.

네이티브 메서드를 실행 중일 경우 PC 값은 undefined 상태가 됩니다.

Native Method Stack

Native Method Stack은 JNI(Java Native Interface)를 통해 호출되는 네이티브 코드(C/C++) 실행을 담당합니다.

1
public native void nativeMethod();

네이티브 코드 실행 중 오류가 발생하면 Java 스택이 아닌 Native Stack에서 문제가 발생합니다.

1
2
3
4
5
6
7
8
9
public class NativeMethodExample {
    // native 키워드로 선언된 메서드는 네이티브 코드로 구현됨
    public native void nativeMethod();
 
    static {
        // 네이티브 라이브러리 로드
        System.loadLibrary("nativeLib");
    }
}

런타임 데이터 영역과 장애 유형 매핑

JVM 구조를 이해해야 하는 가장 현실적인 이유는 장애 원인을 빠르게 좁히기 위해서입니다.

영역 대표 오류 주요 원인
Heap OutOfMemoryError 객체 누수, GC 미회수
Stack StackOverflowError 깊은 재귀, 큰 스택 프레임
Metaspace OOM: Metaspace ClassLoader 누수
Code Cache CodeCache Full JIT 과다 컴파일
Native Stack JVM Crash JNI 오류

로그만 보고도 “어느 영역 문제인지” 감이 오면 이미 JVM을 이해하고 있는 개발자입니다.

실행 엔진 (Execution Engine)

실행 엔진은 런타임 데이터 영역에 로드된 바이트코드를
실제로 실행하는 JVM의 핵심 컴포넌트입니다.

실행 엔진은 다음 세 가지 요소로 구성됩니다.

  • Interpreter
  • JIT Compiler
  • Garbage Collector

Interpreter (인터프리터)

인터프리터는 바이트코드를 한 줄씩 읽어 즉시 실행합니다.
컴파일 과정이 필요 없기 때문에 초기 실행 속도는 빠르지만,
같은 코드를 반복 실행할 때도 매번 해석해야 하므로 성능이 떨어집니다.

JVM은 애플리케이션 시작 시
일단 빠르게 실행시키는 것을 우선으로 인터프리터를 사용합니다.


JIT Compiler (Just-In-Time Compiler)

JIT 컴파일러는 인터프리터의 단점을 보완하기 위해 도입되었습니다.
JVM은 실행 중 메서드 호출 횟수와 루프 반복 횟수를 기록하며
자주 실행되는 코드(HotSpot) 를 감지합니다.

핫스팟으로 판단된 코드는 다음 과정을 거칩니다.

  1. 인터프리터 실행
  2. 실행 횟수 카운팅 (Profiling)
  3. 임계값 초과 → HotSpot 판단
  4. JIT 컴파일 → 네이티브 코드 생성
  5. Code Cache 저장
  6. 이후 네이티브 코드 직접 실행

JIT 컴파일이 빠른 이유 (최적화 예시)

JIT는 단순 변환기가 아니라
실행 패턴을 기반으로 코드를 재작성합니다.

Method Inlining

자주 호출되는 작은 메서드를 호출 대신 본문 코드로 치환합니다.

1
2
3
int sum() {
    return a + b;
}

→ 호출 비용 제거

Escape Analysis

객체가 메서드 밖으로 탈출하지 않는다고 판단되면 힙 할당을 스택 할당으로 변경합니다.

1
2
3
void calc() {
    Point p = new Point(1, 2);
}

→ GC 대상 객체 제거

Loop Optimization

  • 반복문 전개(Loop Unrolling)
  • 불필요한 범위 체크 제거

이러한 최적화는 실행하면서만 알 수 있는 정보를 기반으로 이루어집니다.

Tiered Compilation

HotSpot JVM은 두 가지 JIT 컴파일러를 사용합니다.

컴파일러 특징
C1 빠른 컴파일, 낮은 최적화
C2 느린 컴파일, 높은 최적화

Tiered Compilation은 초기에는 C1 → 충분히 뜨거워지면 C2로 재컴파일하는 전략입니다.

Code Cache와 장애 포인트

JIT 결과물은 Code Cache에 저장됩니다. Code Cache가 가득 차면 다음과 같은 경고가 발생할 수 있습니다.

1
CodeCache is full. Compiler has been disabled.

이 경우 애플리케이션 성능이 급격히 저하됩니다.

1
-XX:ReservedCodeCacheSize=256m

Garbage Collector (가비지 컬렉터)

가비지 컬렉터는 힙 영역에서 더 이상 참조되지 않는 객체를 자동으로 회수합니다.

GC는 JVM 성능에 직접적인 영향을 미치며, Stop-the-World 현상을 동반합니다.

GC는 있어도 문제, 없어도 문제인 존재입니다.

가비지 컬렉터는 힙 영역에서 더 이상 참조되지 않는 객체를 자동으로 탐지하고 메모리에서 제거합니다.

JVM vs Android Runtime (Dalvik / ART)

JVM은 스택 기반 가상 머신입니다. 반면 Android Runtime(Dalvik / ART)은 레지스터 기반 VM입니다.

구분 JVM Dalvik / ART
구조 Stack 기반 Register 기반
목적 서버 / 데스크톱 모바일
특징 플랫폼 독립성 전력 효율

모바일 환경에서는 레지스터 기반이 전력 소모 측면에서 유리합니다.

JNI (Java Native Interface)

JNI는 자바와 네이티브 코드(C, C++ 등) 간의 상호작용을 가능하게 하는 인터페이스입니다. JNI를 통해 자바에서 네이티브 라이브러리의 함수를 호출하거나, 반대로 네이티브 코드에서 자바 메서드를 호출할 수 있습니다.

JNI는 주로 다음과 같은 경우에 사용됩니다. 첫째, 플랫폼 특화 기능을 사용해야 할 때입니다. 예를 들어, 특정 운영체제의 시스템 콜을 직접 호출해야 하는 경우가 있습니다. 둘째, 성능이 중요한 연산을 C/C++로 구현해야 할 때입니다. 셋째, 기존 네이티브 라이브러리를 자바에서 활용해야 할 때입니다.

자바 프로그램의 실행 과정 정리

지금까지 살펴본 내용을 바탕으로 자바 프로그램이 실행되는 전체 과정을 정리해보겠습니다.

  1. 소스 코드 작성: 개발자가 .java 파일에 자바 소스 코드를 작성합니다.

  2. 컴파일: javac 컴파일러가 소스 코드를 컴파일하여 .class 바이트코드 파일을 생성합니다. 이 과정에서 문법 오류가 있으면 컴파일 에러가 발생합니다.

  3. 클래스 로딩: JVM이 실행되면 클래스 로더가 필요한 클래스들을 메모리에 로드합니다. 로딩, 링킹, 초기화 과정을 거쳐 클래스가 사용 가능한 상태가 됩니다.

  4. 바이트코드 실행: 실행 엔진이 바이트코드를 실행합니다. 인터프리터가 한 줄씩 해석하여 실행하고, 자주 실행되는 코드는 JIT 컴파일러에 의해 네이티브 코드로 컴파일됩니다.

  5. 메모리 관리: 프로그램 실행 중에 생성된 객체들은 힙에 저장되고, 가비지 컬렉터가 주기적으로 사용되지 않는 객체를 정리합니다.

  6. 프로그램 종료: main 메서드가 종료되거나, System.exit()가 호출되거나, 예외가 처리되지 않으면 JVM이 종료됩니다.

면접에서 자주 나오는 질문과 답변

JVM, JRE, JDK의 차이점은 무엇인가요?

  • JVM(Java Virtual Machine) 은 자바 바이트코드를 실행하는 가상 머신입니다. 플랫폼마다 다른 JVM 구현체가 있으며, 바이트코드를 해당 플랫폼의 기계어로 변환하여 실행합니다.

  • JRE(Java Runtime Environment) 는 JVM과 자바 표준 클래스 라이브러리를 포함한 실행 환경입니다. 자바 프로그램을 실행하려면 최소한 JRE가 설치되어 있어야 합니다.

  • JDK(Java Development Kit) 는 JRE에 더해 컴파일러(javac), 디버거, 각종 개발 도구를 포함한 개발 환경입니다. 자바 프로그램을 개발하려면 JDK가 필요합니다.

Stack과 Heap의 차이점은 무엇인가요?

Stack은 스레드별로 생성되며, 메서드 호출 시 생성되는 지역 변수와 매개변수가 저장됩니다. 메서드가 종료되면 해당 스택 프레임이 자동으로 제거되므로 메모리 관리가 간단합니다. 접근 속도가 빠르지만, 크기가 제한되어 있습니다.

Heap은 모든 스레드가 공유하며, 객체와 배열이 저장됩니다. 가비지 컬렉터에 의해 메모리가 관리되며, Stack보다 크기가 크고 동적으로 할당됩니다. 다만 접근 속도는 Stack보다 느리고, 메모리 단편화가 발생할 수 있습니다.

클래스 로더의 위임 모델이란 무엇인가요?

클래스 로더의 위임 모델은 클래스 로딩 요청이 들어왔을 때 부모 클래스 로더에게 먼저 위임하는 방식입니다. 부모 클래스 로더가 해당 클래스를 찾지 못하면 자식 클래스 로더가 직접 로딩을 시도합니다.

이 모델의 장점은 보안입니다. 예를 들어, 악의적인 사용자가 java.lang.String 클래스를 자신만의 구현으로 대체하려고 해도, Bootstrap ClassLoader가 먼저 정상적인 String 클래스를 로드하므로 대체가 불가능합니다.

JIT 컴파일러는 어떻게 동작하나요?

JIT 컴파일러는 자주 실행되는 코드(핫스팟) 를 감지하여 네이티브 코드로 컴파일합니다. JVM은 메서드가 호출될 때마다 카운터를 증가시키고, 카운터가 특정 임계값을 초과하면 해당 메서드를 핫스팟으로 판단합니다.

핫스팟으로 판단된 메서드는 JIT 컴파일러에 의해 최적화된 네이티브 코드로 컴파일되고, 이 코드는 코드 캐시에 저장됩니다. 이후 같은 메서드가 호출될 때는 인터프리터를 거치지 않고 캐시된 네이티브 코드가 직접 실행되어 성능이 향상됩니다.

마무리

이 글에서는 JVM의 구조와 동작 원리에 대해 상세히 살펴보았습니다. JVM을 이해하는 것은 자바 개발자로서 메모리 관련 문제를 해결하고, 애플리케이션 성능을 최적화하는 데 필수적입니다.

특히 클래스 로더의 동작 방식, 런타임 데이터 영역의 구조, 그리고 실행 엔진의 역할을 이해하면 OutOfMemoryError나 StackOverflowError 같은 오류가 발생했을 때 원인을 파악하고 해결하는 데 큰 도움이 됩니다.

다음 글에서는 가비지 컬렉션에 대해 더 자세히 다루겠습니다. 가비지 컬렉션은 JVM의 핵심 기능 중 하나로, 이를 이해하면 메모리 누수를 방지하고 GC 튜닝을 통해 애플리케이션 성능을 개선할 수 있습니다.

참고 자료