✅ 전체 메모리 구조 (프로세스 기준)

┌────────────────────────────┐
│        Method Area         │ ← 클래스 메타정보 (공유)
├────────────────────────────┤
│          Heap              │ ← 객체 저장 공간 (공유)
├────────────────────────────┤
│     Thread Stack (개별)     │ ← 지역 변수, 매개변수 (비공유)
├────────────────────────────┤
│       PC Register (개별)    │ ← 현재 실행 중인 명령어 위치
└────────────────────────────┘

1. Method Area (클래스 영역)

  • 저장 내용: 클래스 정보, static 변수, 메서드 바이트코드 등
  • 공유 여부: 모든 쓰레드가 공유
  • 용도: 클래스 로딩 시 한 번만 로딩, 전체 앱에서 재사용
  • 예시: static 변수, 클래스 구조, 상수 풀 등

✅ JVM에서 이 영역은 “메타스페이스(Metaspace)”라고도 불림 (Java 8부터)

2. Heap Area (힙 메모리)

  • 저장 내용: new로 생성한 객체, 배열 등 동적 할당 메모리
  • 공유 여부: 모든 쓰레드가 공유
  • 용도: GC(Garbage Collection) 대상, 객체 저장소
  • 예시: new String("hello"), DAO, DTO 객체 등

✅ GC(Garbage Collector)는 이 영역만 수집함

3. Stack Area (스택 메모리, 쓰레드별)

  • 저장 내용: 지역 변수, 매개변수, 함수 호출 정보 (프레임)
  • 공유 여부: 쓰레드마다 독립적 (비공유)
  • 용도: 메서드 실행마다 새로운 스택 프레임 생성
  • 예시: int x = 10, String name 같은 지역 변수 등

📌 메서드 호출 시: → 스택 프레임 생성 → 변수 저장 → 메서드 종료 시: 해당 스택 프레임 제거 (LIFO 구조)

4. PC Register (Program Counter Register)

  • 저장 내용: 현재 실행 중인 JVM 명령어 주소
  • 공유 여부: 쓰레드 개별 (비공유)
  • 용도: 쓰레드가 현재 실행 중인 바이트코드 명령 추적
  • 특징: 한 쓰레드는 한 시점에 한 명령만 수행 -> PC 레지스터 필요

💡 메모리 구조를 왜 알아야 하는가?

  • 스레드 안정성 이해: Heap은 공유되므로 동기화 필요 (synchronized, volatile)
  • GC 최적화 전략: 객체 수명, Young/Old 구분도 heap 기반
  • 성능 분석: Stack overflow, OutOfMemoryError 이해에 핵심
  • 스레드 디버깅: 각 스택 프레임에서 메서드 추적 가능

📌 예시로 보는 메모리 흐름 (Java 기준)

public class Example {
    public static void main(String[] args) {
        int a = 5; // Stack
        String s = new String("hello"); // Heap
    }
}
  • int a: 스택 영역에 저장됨 (main 스레드 전용)
  • new String(...): 힙에 저장, 참조변수 s는 스택에 위치
  • String.class: 메서드 영역에 저장됨

🔍 GC란?

🔸 GC란?

GC는 JVM이 Heap 메모리 안에서 더 이상 사용되지 않는 객체를 자동으로 수거하여 메모리를 회수하는 메커니즘이다.

  • 개발자가 직접 메모리 해제를 하지 않아도 됨 (delete()free() 불필요)
  • 메모리 누수, OutOfMemoryError 방지를 위한 핵심 시스템

🔸 어떤 객체를 수거 대상으로 판단하나?

GC는 더 이상 참조되지 않는 객체를 찾아내어 제거한다.

User user = new User(); // 객체 생성됨 → Heap 저장
user = null;            // 참조 끊김 → GC 대상

👉 이처럼 어느 변수도 해당 객체를 참조하지 않으면 도달 불가능 -> GC가 수거

이 원칙을 Reachability Analysis (도달 가능성 분석)이라고 한다.

🔸 GC는 Heap만 관리한다

메모리 영역 GC 대상 여부
Heap ✅ 대상 (객체 저장소)
Stack ❌ 대상 아님 (메서드 호출 끝나면 자동 해제됨)
Method Area ❌ 대상 아님 (클래스 메타 정보 유지용)

🔸 Heap 내부 구조: GC 효율을 위한 세대 분리

Heap
├── Young Generation
│   ├── Eden
│   └── Survivor (S0, S1)
└── Old Generation
  • Young: 새로 생성된 객체. 대부분 여기서 짧게 살다 죽음 -> Minor GC 대상
  • Old: 오래 살아남은 객체. Young에서 여러 번 살아남아 승격됨 -> Major GC 대상
영역 설명
Eden 새로 생성된 객체의 최초 저장소. GC 대상
S0/S1 Eden에서 살아남은 객체의 중간 임시 저장소
Old 여러 번 살아남은 객체의 장기 저장소. Major GC 대상

이 구조 덕분에 JVM은 짧은 생명주기를 가진 객체는 빠르게 수거하고, 오래 살아남은 객체는 안정적으로 유지하는 방식으로 메모리 효율을 최적화할 수 있다.

🔸 GC의 종류

GC 타입 대상 특징
Minor GC Young 영역 빠르고 자주 발생함
Major GC Old 영역 느리고 자주 발생하면 위험
Full GC Heap 전체 애플리케이션 정지 시간 발생 (Pause the World)

🔸 GC는 왜 중요할까?

  • 힙 메모리 한정 -> 효율적인 회수 전략이 중요
  • GC 시간 = 애플리케이션 중단 시간이 될 수 있음 (Pause the World)
  • Spring Boot처럼 많은 Bean을 생성하고 파괴하는 구조에선 GC 튜닝이 필수

🚀 Spring 관점에서 보는 Heap 메모리 관리

Spring 애플리케이션은 수많은 객체(Bean)를 직접 생성하고 관리한다. 이 객체들은 일반 Java 객체와 다르지 않으며, 결국 모두 Heap 메모리에 저장된다. 하지만 Spring이 이 객체들의 생성과 주입, 생명주기 관리까지 맡아주기 때문에 개발자는 직접 메모리를 관리하지 않고도 복잡한 애플리케이션을 안정적으로 운영할 수 있다.

✅ Bean 객체는 Heap에 저장된다

@Service
public class UserService {
    // 이 클래스의 인스턴스는 Spring이 생성 → Heap에 저장됨
}
  • Spring은 ApplicationContext를 통해 이 UserService 객체를 생성
  • 생성된 인스턴스는 Heap 메모리 상에 저장되고, 애플리케이션이 살아있는 동안 재사용됨
  • 기본 스코프인 Singleton의 경우, 딱 한 번 생성되어 전역적으로 공유됨

✅ GC는 Spring이 객체 참조를 해제한 이후에 개입한다

  • Spring이 더 이상 해당 Bean을 참조하지 않게 되고, 개발자 코드에서도 참조하지 않으면
  • JVM의 GC가 이 객체를 Heap에서 제거할 수 있다.
  • 하지만 대부분의 Spring Bean은 애플리케이션 종료 시까지 살아있기 때문에, GC 대상이 되는 경우는 제한적이다.

✅ Spring이 관리하지 않는 객체도 Heap에 올라간다

public List<User> getUserList() {
    return new ArrayList<>(); // 직접 생성한 객체 → Heap 저장
}
  • 이런 일반 객체들도 모두 Heap에 저장되고,
  • Spring이 아닌 개발자 코드나 로직이 참조를 해제하면 GC가 회수하게 된다.

🧠 결론

Spring 애플리케이션은 클래스 기반으로 수많은 Bean을 생성하고 DI를 통해 조립하지만, 이 모든 객체는 결국 JVM의 Heap 메모리에 올라가며, Spring이 그 생명주기를 통제하고, JVM GC는 참조가 사라졌을 때만 이를 회수한다.

따라서 Spring 개발자에게 JVM 메모리 구조와 GC의 동작 원리를 이해하는 것은 애플리케이션 성능 튜닝과 메모리 최적화에 매우 중요하다.

태그:

카테고리:

업데이트:

댓글남기기