이 글에서 얻는 것

  • JVM의 메모리 영역(Runtime Data Areas: Stack/Heap/Metaspace)을 “정의”가 아니라 장애/성능 문제와 연결된 모델로 이해합니다.
  • OutOfMemoryError가 “힙 부족”만이 아니라, Metaspace/Direct Memory/Thread Stack 등 여러 원인으로 나뉜다는 감각을 잡습니다.
  • heap dump / thread dump / GC 로그를 어떤 순서로 보면 되는지, 최소한의 디버깅 루틴을 갖춥니다.

0) 이 주제가 필요한 순간(실무 신호)

아래 신호가 보이면 JVM 메모리 모델이 “필수”가 됩니다.

  • 갑자기 응답이 느려지고 GC 로그에 pause가 길어진다
  • java.lang.OutOfMemoryError: Java heap space / Metaspace / GC overhead limit exceeded
  • StackOverflowError가 특정 요청/작업에서 반복된다
  • 컨테이너(K8s)에서 메모리 제한에 걸려 OOMKilled가 난다(힙만 봐서는 해결이 안 됨)

1. JVM 아키텍처 전체 구조

┌─────────────────────────────────────────────────────────┐
│                    Java Application                      │
└────────────────────────┬────────────────────────────────┘
                         │ .class files
┌────────────────────────▼────────────────────────────────┐
│                   JVM (Java Virtual Machine)             │
│                                                          │
│  ┌────────────────────────────────────────────────┐     │
│  │          1. Class Loader Subsystem             │     │
│  │  - Loading → Linking → Initialization          │     │
│  └────────────────────┬───────────────────────────┘     │
│                       │ Loaded Classes                   │
│  ┌────────────────────▼───────────────────────────┐     │
│  │          2. Runtime Data Areas                 │     │
│  │                                                 │     │
│  │  ┌──────────────┐  ┌──────────────┐           │     │
│  │  │ Method Area  │  │     Heap     │           │     │
│  │  │ (Metaspace)  │  │ (Young/Old)  │           │     │
│  │  └──────────────┘  └──────────────┘           │     │
│  │                                                 │     │
│  │  ┌──────────────┐  ┌──────────────┐           │     │
│  │  │   PC Register│  │  JVM Stack   │           │     │
│  │  └──────────────┘  └──────────────┘           │     │
│  │                                                 │     │
│  │  ┌──────────────┐                              │     │
│  │  │ Native Stack │                              │     │
│  │  └──────────────┘                              │     │
│  └────────────────────┬───────────────────────────┘     │
│                       │ Bytecode                         │
│  ┌────────────────────▼───────────────────────────┐     │
│  │          3. Execution Engine                   │     │
│  │  - Interpreter                                 │     │
│  │  - JIT Compiler (C1, C2)                       │     │
│  │  - Garbage Collector                           │     │
│  └────────────────────┬───────────────────────────┘     │
│                       │ Native Method                    │
│  ┌────────────────────▼───────────────────────────┐     │
│  │      4. Java Native Interface (JNI)            │     │
│  └────────────────────────────────────────────────┘     │
└──────────────────────────────────────────────────────────┘
┌────────────────────────▼────────────────────────────────┐
│              Native Method Libraries                     │
│              (C/C++ Libraries)                           │
└─────────────────────────────────────────────────────────┘

2. Class Loader Subsystem

2.1 Class Loading 3단계

1단계: Loading (로딩)

// Example: Class Loading 과정
public class Main {
    public static void main(String[] args) {
        // 1. Bootstrap ClassLoader: java.lang.String (JDK 클래스)
        String str = "Hello";

        // 2. Extension ClassLoader: javax.* (확장 클래스)
        // java.util.logging.Logger

        // 3. Application ClassLoader: 사용자 정의 클래스
        User user = new User();  // User.class 로딩
    }
}

ClassLoader 계층 구조:

┌─────────────────────────┐
│ Bootstrap ClassLoader   │  ← JDK 기본 클래스 (rt.jar)
│ (Native C++)            │     java.lang.*, java.util.*
└──────────┬──────────────┘
┌──────────▼──────────────┐
│ Extension ClassLoader   │  ← 확장 클래스 (jre/lib/ext)
│ (sun.misc.Launcher)     │     javax.*, org.xml.*
└──────────┬──────────────┘
┌──────────▼──────────────┐
│ Application ClassLoader │  ← 사용자 클래스 (Classpath)
│ (sun.misc.Launcher)     │     com.example.*
└──────────┬──────────────┘
┌──────────▼──────────────┐
│ Custom ClassLoader      │  ← 사용자 정의
│ (User Defined)          │     Plugin, Hot Reload
└─────────────────────────┘

ClassLoader 동작 확인:

public class ClassLoaderDemo {
    public static void main(String[] args) {
        // 1. Bootstrap ClassLoader (null 반환)
        ClassLoader stringLoader = String.class.getClassLoader();
        System.out.println("String ClassLoader: " + stringLoader);  // null

        // 2. Extension ClassLoader
        ClassLoader extLoader = com.sun.crypto.provider.DESKeyFactory.class.getClassLoader();
        System.out.println("Extension ClassLoader: " + extLoader);
        // sun.misc.Launcher$ExtClassLoader

        // 3. Application ClassLoader
        ClassLoader appLoader = ClassLoaderDemo.class.getClassLoader();
        System.out.println("Application ClassLoader: " + appLoader);
        // sun.misc.Launcher$AppClassLoader

        // 4. 부모 ClassLoader 조회
        System.out.println("App ClassLoader Parent: " + appLoader.getParent());
        // Extension ClassLoader
    }
}

2단계: Linking (연결)

Linking 3단계:

1. Verification (검증)
   - Bytecode Verifier가 .class 파일 검증
   - 올바른 형식인지, 보안 위반 없는지 확인
   - 예: final 클래스 상속 시도 → VerifyError

2. Preparation (준비)
   - static 변수에 메모리 할당 (Method Area)
   - 기본값으로 초기화 (int = 0, boolean = false)

3. Resolution (해석)
   - Symbolic Reference → Direct Reference 변환
   - 예: "com/example/User" → Heap의 실제 주소

예제:

public class LinkingExample {
    static int count = 10;  // Preparation: count = 0
                            // Initialization: count = 10

    public static void main(String[] args) {
        System.out.println(count);  // 10
    }
}

3단계: Initialization (초기화)

public class InitializationOrder {
    // 1. static 변수 선언 및 초기화
    static int x = 10;

    // 2. static 블록 실행
    static {
        System.out.println("Static block executed");
        x = 20;
    }

    // 3. 인스턴스 변수
    int y = 30;

    // 4. 인스턴스 초기화 블록
    {
        System.out.println("Instance block executed");
        y = 40;
    }

    // 5. 생성자
    public InitializationOrder() {
        System.out.println("Constructor executed");
        y = 50;
    }

    public static void main(String[] args) {
        System.out.println("Main method");
        new InitializationOrder();
    }
}

// 출력 순서:
// Static block executed
// Main method
// Instance block executed
// Constructor executed

2.2 ClassLoader 원칙

1. Delegation Principle (위임 원칙)

// ClassLoader는 항상 부모에게 먼저 위임
public Class<?> loadClass(String name) throws ClassNotFoundException {
    // 1. 이미 로드되었는지 확인
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        // 2. 부모 ClassLoader에게 위임
        if (parent != null) {
            c = parent.loadClass(name);
        } else {
            // 3. Bootstrap ClassLoader
            c = findBootstrapClass(name);
        }
        if (c == null) {
            // 4. 부모가 로드 실패 → 본인이 로드
            c = findClass(name);
        }
    }
    return c;
}

2. Visibility Principle (가시성 원칙)

하위 ClassLoader는 상위의 클래스를 볼 수 있지만,
상위는 하위의 클래스를 볼 수 없다.

Application ClassLoader → Extension ClassLoader 클래스 접근 가능 ✅
Extension ClassLoader → Application ClassLoader 클래스 접근 불가 ❌

3. Uniqueness Principle (유일성 원칙)

// 같은 ClassLoader가 동일한 클래스를 2번 로드하지 않음
Class<?> class1 = classLoader.loadClass("com.example.User");
Class<?> class2 = classLoader.loadClass("com.example.User");

System.out.println(class1 == class2);  // true (같은 인스턴스)


👉 다음 편: JVM 메모리 (Part 2: Runtime Data Areas, GC)