이 글에서 얻는 것
- 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 exceededStackOverflowError가 특정 요청/작업에서 반복된다- 컨테이너(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 (같은 인스턴스)
💬 댓글