Java

JVM 메모리

Daesiker 2025. 11. 4. 10:42
반응형

클래스 로딩 메커니즘의 전체 흐름

Java 프로그램을 실행하면 JVM은 즉시 모든 클래스를 메모리에 올리지 않습니다. 필요한 시점에 동적으로 로딩하는 Lazy Loading 방식을 사용하죠. 이 과정은 Loading, Linking, Initialization 세 단계로 구성됩니다.

Loading (로딩): 클래스 로더가 .class 파일을 찾아 바이너리 데이터를 읽고 메서드 영역에 저장합니다. 이때 클래스의 FQCN(Fully Qualified Class Name), 부모 클래스 정보, 메서드와 변수 정보, 접근 제어자 등의 메타데이터가 저장됩니다. 동시에 힙 영역에는 이 클래스를 나타내는 Class 객체가 생성되며, 이 객체는 리플렉션 API의 진입점이 됩니다.

Linking (링킹): 링킹은 다시 세 단계로 나뉩니다. Verification 단계에서는 바이트코드가 Java 언어 명세와 JVM 명세를 준수하는지 검증합니다. 악의적인 코드나 컴파일러 버그로 인한 잘못된 바이트코드를 걸러내는 중요한 보안 계층입니다. Preparation 단계에서는 클래스 변수(static 변수)를 위한 메모리를 할당하고 기본값으로 초기화합니다. 예를 들어 static int count;는 이 시점에 0으로 초기화됩니다. Resolution 단계에서는 심볼릭 레퍼런스를 실제 메모리 주소로 변환합니다. 이 단계는 선택적으로 지연될 수 있습니다.

Initialization (초기화): 클래스 변수가 프로그래머가 의도한 값으로 초기화됩니다. static 초기화 블록이 실행되며, 부모 클래스가 아직 초기화되지 않았다면 부모 클래스를 먼저 초기화합니다. 이 과정은 스레드 안전하게 수행되며, 멀티스레드 환경에서도 클래스는 단 한 번만 초기화됩니다.

 

JVM 메모리 구조 한눈에 보기

먼저 전체 그림을 이해하는 것이 중요합니다. JVM 메모리는 크게 스레드 공유 영역스레드 개별 영역으로 나뉩니다.

┌─────────────────────────────────────────────────────────┐
│                       JVM Memory                        │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌────────────────────────────────────────────────┐     │
│  │         Heap (모든 스레드 공유)                    │     │
│  │  ┌──────────────┐  ┌──────────────────────┐    │     │
│  │  │    Young     │  │        Old           │    │     │
│  │  │  Generation  │  │     Generation       │    │     │
│  │  │              │  │                      │    │     │
│  │  │ Eden    S0│S1│  │   장수한 객체들         │    │     │
│  │  │ new instance |  │                      │    │     │
│  │  └──────────────┘  └──────────────────────┘    │     │
│  └────────────────────────────────────────────────┘     │
│                                                         │
│  ┌────────────────────────────────────────────────┐     │
│  │    Metaspace (모든 스레드 공유)                    │     │
│  │    - 클래스 메타데이터                              │     │
│  │    - static 변수                                │     │
│  │    - 상수 풀                                     │     │
│  └────────────────────────────────────────────────┘     │
│                                                         │
├───────────────┬───────────────┬─────────────────────────┤
│  Thread 1     │  Thread 2     │  Thread 3               │
│ ┌───────────┐ │ ┌───────────┐ │ ┌───────────┐           │
│ │   Stack   │ │ │   Stack   │ │ │   Stack   │           │
│ │ ┌───────┐ │ │ │ ┌───────┐ │ │ │ ┌───────┐ │           │
│ │ │Method3│ │ │ │ │Method1│ │ │ │ │Method2│ │           │
│ │ ├───────┤ │ │ │ └───────┘ │ │ │ ├───────┤ │           │
│ │ │Method2│ │ │ │           │ │ │ │Method1│ │           │
│ │ ├───────┤ │ │ │           │ │ │ └───────┘ │           │
│ │ │Method1│ │ │ │           │ │ │           │           │
│ │ └───────┘ │ │ │           │ │ │           │           │
│ └───────────┘ │ └───────────┘ │ └───────────┘           │
│               │               │                         │
│ ┌───────────┐ │ ┌───────────┐ │ ┌───────────┐           │
│ │PC Register│ │ │PC Register│ │ │PC Register│           │
│ └───────────┘ │ └───────────┘ │ └───────────┘           │
└───────────────┴───────────────┴─────────────────────────┘

Heap 영역

힙은 모든 객체와 배열이 저장되는 곳입니다. 실무에서 가장 많이 마주치는 메모리 이슈가 바로 이 힙 영역에서 발생합니다.

Young Generation - 객체의 탄생지

새로 생성된 대부분의 객체는 여기서 시작합니다. "대부분의 객체는 금방 죽는다"는 Weak Generational Hypothesis에 기반한 설계입니다.

public void processOrders() {
    for (Order order : orders) {
        // 매 루프마다 임시 객체 생성
        OrderValidator validator = new OrderValidator();  // Eden 영역 할당
        OrderResult result = validator.validate(order);   // Eden 영역 할당
        
        // 메서드 종료 후 validator, result는 참조 해제
        // Minor GC 때 정리됨
    }
}
```

**Eden (에덴)**: 새 객체가 최초로 생성되는 곳입니다. 가득 차면 Minor GC가 발생합니다.

**Survivor 0, Survivor 1**: Minor GC에서 살아남은 객체가 이동하는 곳입니다. 두 영역 중 하나는 항상 비어있으며, GC마다 객체들이 번갈아 이동합니다.
```
Minor GC 과정:
1회차: Eden → S0 (age 1)
2회차: Eden + S0 → S1 (age 2)
3회차: Eden + S1 → S0 (age 3)
...
Age 임계값 도달 → Old Generation 승격

Old Generation - 장수 객체의 거처

Young Generation에서 여러 번의 GC를 살아남은 객체가 승격됩니다. 싱글톤 객체, 캐시, 큰 데이터 구조 등이 주로 여기 위치합니다.

public class CacheManager {
    // static 변수의 참조는 Metaspace에, 실제 객체는 Old Generation에
    private static Map<String, User> userCache = new HashMap<>();
    
    public void cacheUser(User user) {
        // 이 User 객체는 계속 참조되므로 결국 Old Generation으로
        userCache.put(user.getId(), user);
    }
}

실무 팁: Old Generation이 가득 차면 Major GC(Full GC)가 발생하는데, 이는 애플리케이션을 멈추게 하는(Stop-The-World) 주요 원인입니다. 특히 수 GB 이상의 힙에서는 수 초가 걸릴 수 있습니다.

Heap 모니터링

# 1. 힙 사용량 확인
jstat -gc <pid> 1000

# 출력 예시:
# S0C    S1C    EC       EU       OC        OU
# 512.0  512.0  4096.0   2048.5   8192.0    3072.1

# EC: Eden Capacity (Eden 크기)
# EU: Eden Used (Eden 사용량)
# OC: Old Capacity (Old 크기)
# OU: Old Used (Old 사용량)

# 2. 힙 덤프 생성 (메모리 분석용)
jmap -dump:live,format=b,file=heap.bin <pid>

# 3. 실시간 모니터링
jvisualvm  # GUI 도구
```

## Stack - 메서드 실행의 공간

### Stack이란?

Stack은 **각 스레드마다 독립적으로 생성**되는 메모리 영역으로, 메서드 호출과 지역 변수를 관리합니다.
```
Thread 1의 Stack:

┌──────────────────┐  ← 스레드 시작 시 생성
│                  │
│  ┌────────────┐  │
│  │ method3()  │  │  ← 가장 최근 호출
│  ├────────────┤  │
│  │ method2()  │  │
│  ├────────────┤  │
│  │ method1()  │  │
│  ├────────────┤  │
│  │  main()    │  │  ← 맨 처음 호출
│  └────────────┘  │
│                  │
└──────────────────┘  ← 스레드 종료 시 소멸

 

 

반응형

Stack의 동작 방식

public class StackExample {
    public static void main(String[] args) {  // Stack Frame 1
        int x = 10;
        String name = "John";
        
        calculate(x);  // Stack Frame 2 생성
        
        System.out.println(name);
    }  // ← Stack Frame 1 제거
    
    static void calculate(int value) {  // Stack Frame 2
        int result = value * 2;
        
        display(result);  // Stack Frame 3 생성
        
    }  // ← Stack Frame 2 제거
    
    static void display(int num) {  // Stack Frame 3
        System.out.println("Result: " + num);
    }  // ← Stack Frame 3 제거
}
```

**실행 흐름**:
```
1. main() 호출
   Stack: [main]
   
2. calculate() 호출
   Stack: [main] → [calculate]
   
3. display() 호출
   Stack: [main] → [calculate] → [display]
   
4. display() 종료
   Stack: [main] → [calculate]
   
5. calculate() 종료
   Stack: [main]
   
6. main() 종료
   Stack: []
```

### Stack Frame의 구성
```
Stack Frame 구조:

┌─────────────────────────────┐
│      calculate() Frame      │
├─────────────────────────────┤
│  Local Variables (지역변수)   │
│  - value: 10                │
│  - result: 20               │
├─────────────────────────────┤
│  Operand Stack (연산 스택)    │
│  - 계산 중인 값들              │
├─────────────────────────────┤
│  Frame Data                 │
│  - 복귀 주소                  │
│  - Exception 정보            │
└─────────────────────────────┘

Stack과 Heap의 상호작용

public class MemoryExample {
    public static void main(String[] args) {
        // Stack: num (값 10 저장)
        int num = 10;
        
        // Stack: str (힙 주소 저장)
        // Heap: "Hello" 문자열 객체
        String str = "Hello";
        
        // Stack: user (힙 주소 저장)
        // Heap: User 객체 (name, age 필드 포함)
        User user = new User("John", 30);
        
        // 메서드 호출
        processUser(user);
        
    } // ← main 메서드 종료 시 스택 프레임 전체 제거
    
    static void processUser(User user) {
        // 새로운 스택 프레임 생성
        // user 파라미터는 같은 힙 객체를 가리킴 (주소 복사)
        
        String message = "Processing: " + user.getName();
        // Stack: message (힙 주소)
        // Heap: "Processing: John" 문자열 객체
        
    } // ← processUser 메서드 종료 시 이 프레임만 제거
}
```

**메모리 배치 시각화**:
```
Stack (Thread 1)              Heap
┌──────────────┐           ┌─────────────────────┐
│ main()       │           │ User Object         │
│  num: 10     │           │  name: "John"       │
│  str: 0x1001 │─────────→ │  age: 30            │
│  user: 0x2001│─────┐     └─────────────────────┘
└──────────────┘     │     
┌──────────────┐     │     ┌─────────────────────┐
│processUser() │     └───→ │ User Object         │
│  user: 0x2001│───────────│ (same)              │
│  message:... │           └─────────────────────┘
└──────────────┘           
                            ┌─────────────────────┐
                            │ "Hello" String      │
                            │  (at 0x1001)        │
                            └─────────────────────┘

 

 

String 클래스의 특별한 메모리 관리 방식

public class StringMemory {
    public static void main(String[] args) {
        
        // 1. String 리터럴 - String Pool (Heap 내부)
        String s1 = "Hello";
        String s2 = "Hello";
        System.out.println(s1 == s2);  // true (같은 주소)
        
        // 2. new String - 새로운 Heap 객체
        String s3 = new String("Hello");
        System.out.println(s1 == s3);  // false (다른 주소)
        System.out.println(s1.equals(s3));  // true (값은 같음)
    }
}
```
```
String 메모리 배치:

Stack                    Heap
┌─────────────┐       ┌──────────────────────┐
│ main()      │       │  String Pool         │
│             │       │  ┌────────────────┐  │
│ s1: 0x1001 ─┼───┬──→│  │ "Hello"        │  │
│ s2: 0x1001 ─┼───┘   │  └────────────────┘  │
│             │       │                      │
│ s3: 0x2001 ─┼───┐   │  Regular Heap        │
│             │   │   │  ┌────────────────┐  │
└─────────────┘   └──→│  │ "Hello"        │  │
                      │  │ (new로 생성)     │  │
                      │  └────────────────┘  │
                      └──────────────────────┘

Heap vs Stack 비교표

구분 Heap Stack
저장 대상 객체, 배열 메서드 호출 정보, 지역 변수
공유 여부 모든 스레드 공유 스레드마다 독립적
생명 주기 GC가 관리 메서드 종료 시 자동 제거
크기 크다 (GB 단위) 작다 (MB 단위)
속도 상대적으로 느림 빠름
에러 OutOfMemoryError StackOverflowError
설정 -Xms, -Xmx -Xss
 
 
반응형

'Java' 카테고리의 다른 글

Java Collection Framework - ArrayList  (0) 2025.12.01
Java BufferedReader와 StringTokenizer  (0) 2025.11.18
Java Array  (1) 2025.11.12
JDK 완벽 이해하기  (1) 2025.10.30