[자바/Java] JVM 동작 원리와 핵심 구조 (1)

dongbrown·2025년 6월 18일

Java

목록 보기
3/6

"Write Once, Run Anywhere" - Java의 이 철학을 가능하게 하는 것이 바로 JVM(Java Virtual Machine)입니다. 오늘은 Java 개발자라면 반드시 알아야 할 JVM의 동작 원리와 내부 구조를 자세히 알아보겠습니다.


🤔 JVM이란 무엇인가?

JVM(Java Virtual Machine)은 Java 프로그램이 실행되는 가상 머신입니다. 물리적인 컴퓨터가 아닌 소프트웨어로 구현된 가상의 컴퓨터로, Java 바이트코드를 해석하고 실행하는 역할을 담당합니다.

JVM의 핵심 역할

  • 플랫폼 독립성: 운영체제에 상관없이 Java 프로그램 실행
  • 메모리 관리: 자동 메모리 할당 및 가비지 컬렉션
  • 보안: 샌드박스 환경에서 안전한 코드 실행
  • 최적화: JIT 컴파일러를 통한 성능 향상

🔄 JVM의 전체 동작 과정

JVM이 Java 프로그램을 실행하는 과정을 단계별로 살펴보겠습니다.

Java 소스코드(.java) 
    ↓ [javac 컴파일러]
바이트코드(.class)
    ↓ [클래스 로더]
런타임 데이터 영역
    ↓ [실행 엔진]
기계어 실행

1단계: 컴파일 과정

// HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("안녕하세요, JVM!");
    }
}

javac 컴파일러가 위의 Java 소스코드를 바이트코드로 변환:

javac HelloWorld.java  # → HelloWorld.class 생성

2단계: JVM 실행 과정

  1. 메모리 할당: JVM이 운영체제로부터 메모리를 할당받습니다
  2. 클래스 로딩: 필요한 클래스 파일들을 메모리에 로드
  3. 바이트코드 실행: 실행 엔진이 바이트코드를 해석하여 실행
  4. 메모리 관리: 가비지 컬렉터가 불필요한 메모리를 정리

🏗️ JVM의 내부 구조

JVM은 크게 다음과 같은 구성 요소로 이루어져 있습니다:


📚 클래스 로더 시스템 (Class Loader)

클래스 로더는 .class 파일을 JVM 메모리에 로드하는 핵심 컴포넌트입니다.

클래스 로더의 3단계 프로세스

1️⃣ Loading (로딩)

  • 클래스 파일을 찾아서 JVM 메모리에 로드
  • 바이너리 데이터를 메모리에 저장
// 예시: 클래스가 처음 사용될 때 로딩됨
public class Example {
    public static void main(String[] args) {
        // 이 시점에서 User 클래스가 로딩됨
        User user = new User(); 
    }
}

2️⃣ Linking (링킹)

링킹은 3개의 하위 단계로 구성됩니다:

a) Verification (검증)

  • 바이트코드가 JVM 명세에 맞는지 검증
  • 보안과 안전성을 위한 필수 과정

b) Preparation (준비)

  • 클래스의 static 변수를 위한 메모리 할당
  • 기본값으로 초기화
public class StaticExample {
    static int number;        // 준비 단계에서 0으로 초기화
    static String text;       // 준비 단계에서 null로 초기화
    static final int CONSTANT = 100; // 컴파일 타임에 이미 값이 결정됨
}

c) Resolution (해석)

  • 심볼릭 참조를 실제 메모리 참조로 변환

3️⃣ Initialization (초기화)

  • static 변수를 실제 값으로 초기화
  • static 블록 실행
public class InitializationExample {
    static int count = 10;    // 초기화 단계에서 10으로 설정
    
    static {
        System.out.println("static 블록 실행");
        count = 20;           // 초기화 단계에서 실행
    }
}

클래스 로더의 계층 구조

Bootstrap Class Loader (최상위)
    ↓
Extension Class Loader
    ↓  
Application Class Loader (기본)
    ↓
Custom Class Loader (사용자 정의)

Bootstrap Class Loader

  • java.lang.*, java.util.* 등 핵심 Java API 로드
  • C/C++로 구현됨

Extension Class Loader

  • 확장 라이브러리 로드 ($JAVA_HOME/lib/ext)

Application Class Loader

  • 애플리케이션 클래스패스의 클래스들 로드
  • 우리가 작성한 대부분의 클래스가 여기서 로드됨

⚙️ 실행 엔진 (Execution Engine)

실행 엔진은 클래스 로더가 메모리에 적재한 바이트코드를 실제로 실행하는 컴포넌트입니다.

바이트코드란?

바이트코드는 JVM이 이해할 수 있는 중간 언어입니다:

// Java 소스코드
public int add(int a, int b) {
    return a + b;
}
// 컴파일된 바이트코드 (javap -c 명령어로 확인 가능)
public int add(int, int);
  Code:
     0: iload_1      // 첫 번째 매개변수를 스택에 로드
     1: iload_2      // 두 번째 매개변수를 스택에 로드  
     2: iadd         // 두 값을 더함
     3: ireturn      // 결과 반환

실행 방식: 인터프리터 vs JIT 컴파일러

🐌 인터프리터 (Interpreter)

  • 바이트코드를 한 줄씩 해석하여 실행
  • 초기 실행 속도는 빠르지만, 반복 실행 시 비효율적
public class InterpreterExample {
    public static void main(String[] args) {
        // 이 루프는 처음에는 인터프리터로 실행됨
        for (int i = 0; i < 1000; i++) {
            calculateSomething(i);
        }
    }
    
    static int calculateSomething(int x) {
        return x * x + 2 * x + 1;
    }
}

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

  • 자주 실행되는 코드를 네이티브 코드로 컴파일
  • 한 번 컴파일 후에는 매우 빠른 실행 속도

JIT 컴파일러의 최적화 과정:

  1. Hot Spot 감지: 자주 실행되는 메서드나 루프 탐지
  2. 컴파일: 해당 부분을 네이티브 코드로 컴파일
  3. 캐싱: 컴파일된 코드를 메모리에 저장
  4. 직접 실행: 이후 호출 시 네이티브 코드로 직접 실행
public class JITExample {
    public static void main(String[] args) {
        // 이 메서드가 충분히 많이 호출되면 JIT 컴파일 대상이 됨
        for (int i = 0; i < 10000; i++) {
            heavyCalculation(i);  // Hot Spot으로 감지될 가능성
        }
    }
    
    static long heavyCalculation(int n) {
        long result = 0;
        for (int i = 0; i < n; i++) {
            result += i * i;
        }
        return result;
    }
}

JIT 컴파일러의 종류

C1 컴파일러 (Client 컴파일러)

  • 빠른 컴파일 시간
  • 기본적인 최적화 수행

C2 컴파일러 (Server 컴파일러)

  • 느린 컴파일 시간
  • 고급 최적화 수행 (인라이닝, 루프 최적화 등)

Tiered Compilation (계층 컴파일)

  • C1과 C2를 조합하여 사용
  • 초기에는 C1으로 빠르게 컴파일, 나중에 C2로 재컴파일

🧹 가비지 컬렉터 (Garbage Collector)

가비지 컬렉터는 더 이상 사용되지 않는 객체를 자동으로 메모리에서 제거하는 JVM의 핵심 기능입니다.

가비지 컬렉션이 필요한 이유

public class GCExample {
    public static void main(String[] args) {
        for (int i = 0; i < 1000000; i++) {
            // 매 반복마다 새로운 String 객체 생성
            String temp = new String("임시 문자열 " + i);
            // temp는 루프가 끝나면 더 이상 참조되지 않음 → GC 대상
        }
        // 이 시점에서 GC가 작동하여 불필요한 String 객체들을 정리
    }
}

GC의 기본 원리

Mark and Sweep 알고리즘
1. Mark: 사용 중인 객체들을 마킹
2. Sweep: 마킹되지 않은 객체들을 메모리에서 제거
3. Compact: 메모리 조각화 해결을 위한 압축 (선택적)

참조 계산 vs 도달 가능성

public class ReferenceExample {
    public static void main(String[] args) {
        // obj1과 obj2는 서로 참조 (순환 참조)
        CircularRef obj1 = new CircularRef();
        CircularRef obj2 = new CircularRef();
        obj1.ref = obj2;
        obj2.ref = obj1;
        
        // 지역 변수를 null로 설정
        obj1 = null;
        obj2 = null;
        
        // 순환 참조가 있지만, GC Root에서 도달할 수 없으므로 GC 대상
    }
}

class CircularRef {
    CircularRef ref;
}

GC Root 객체들:

  • Stack 영역의 지역 변수
  • Method Area의 static 변수
  • JNI에서 생성한 객체

🎯 JVM 최적화 팁

1. 적절한 힙 크기 설정

# 힙 크기 설정 예시
java -Xms512m -Xmx2g MyApplication

# -Xms: 초기 힙 크기
# -Xmx: 최대 힙 크기

2. GC 로그 활성화

# GC 로그 설정 (Java 8)
java -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps MyApp

# GC 로그 설정 (Java 11+)
java -Xlog:gc*:gc.log:time MyApp

3. 메서드 최적화를 위한 팁

public class OptimizationTips {
    
    // ✅ 좋은 예: 메서드가 자주 호출되어 JIT 최적화 대상
    public static int fastCalculation(int x) {
        return x * x + 2 * x + 1;  // 간단한 연산
    }
    
    // ❌ 나쁜 예: 너무 큰 메서드는 JIT 최적화가 어려움
    public static void hugeMothod() {
        // 수백 줄의 코드...
        // JIT 컴파일러가 최적화하기 어려움
    }
    
    // ✅ 좋은 예: final 메서드는 인라이닝 최적화 가능
    public final int inlineableMethod(int x) {
        return x + 1;
    }
}

🔍 다음 편 미리보기

1편에서는 JVM의 전체적인 동작 과정과 클래스 로더, 실행 엔진, 가비지 컬렉터의 기본 개념을 다뤘습니다.

2편에서 다룰 내용:

  • 📚 런타임 데이터 영역 상세 분석
    • Method Area, Heap, Stack, PC Register 심화 학습
    • 각 영역의 메모리 구조와 데이터 저장 방식
  • 🧠 메모리 관리 실전 가이드
    • 힙 메모리의 Young Generation, Old Generation
    • 다양한 GC 알고리즘과 성능 튜닝
  • 🔧 JVM 모니터링과 성능 분석
    • 메모리 누수 탐지 방법
    • 실제 운영 환경에서의 JVM 튜닝 사례

💡 마무리

JVM의 동작 원리를 이해하면 Java 애플리케이션의 성능 문제를 진단하고 해결하는 데 큰 도움이 됩니다. 특히 메모리 사용량이 많은 애플리케이션이나 고성능이 요구되는 시스템을 개발할 때 JVM에 대한 깊은 이해는 필수입니다.

다음 편에서는 JVM의 메모리 구조를 더 자세히 살펴보고, 실제 개발 현장에서 활용할 수 있는 최적화 기법들을 알아보겠습니다!

0개의 댓글