[CS] JVM 메모리 구조와 Spring Heap 관리까지 한 번에 정리
✅ 전체 메모리 구조 (프로세스 기준)
┌────────────────────────────┐
│ 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의 동작 원리를 이해하는 것은 애플리케이션 성능 튜닝과 메모리 최적화에 매우 중요하다.
댓글남기기