| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 프로그래머스
- 백준
- 개발공부
- 자바
- 코딩테스트
- 자료구조
- 예외처리
- 프로그래밍기초
- 개발자취업
- JVM
- HashMap
- 자바공부
- 멀티스레드
- 코딩테스트준비
- 알고리즘공부
- 파이썬
- 개발자팁
- 코딩인터뷰
- 정렬
- Java
- 자바개발
- 코딩공부
- 자바프로그래밍
- 메모리관리
- 객체지향
- 가비지컬렉션
- 알고리즘
- 클린코드
- 자바기초
- 코딩테스트팁
- Today
- Total
코드 한 줄의 기록
[자바/Java] JVM 메모리 구조 해부: Stack, Heap, Metaspace 동작 원리와 특징 완벽 정리 본문
안녕하세요! 오늘도 즐겁게 코딩하고 계신가요?
개발을 하다 보면 기능 구현에 급급해 정작 우리가 짠 코드가 '어디에', '어떻게' 저장되고 실행되는지 놓칠 때가 많습니다. 저 역시 주니어 시절에는 "그냥 객체는 new 하면 생기는 거 아니야?"라고 단순하게 생각했었는데요. 트래픽이 몰리는 상황에서 OutOfMemoryError를 겪거나, 성능 튜닝 이슈를 마주하게 되니 JVM(Java Virtual Machine)의 메모리 구조를 모르면 해결할 수 없는 벽이 있다는 걸 깨달았습니다.
오늘은 제가 최근에 다시 깊게 공부하며 정리한 JVM 메모리 구조(Runtime Data Area)에 대해 공유해 보려 합니다. 단순히 이론적인 정의를 넘어, 실제 코드가 실행될 때 Stack, Heap, 그리고 Java 8 이후 중요해진 Metaspace가 어떻게 유기적으로 움직이는지 함께 파헤쳐 보겠습니다.
JVM 메모리, 왜 알아야 할까요?
우리가 작성한 .java 파일은 컴파일러에 의해 .class (바이트코드) 파일로 변환됩니다. 이 바이트코드를 OS가 이해할 수 있도록 해석하고 실행하는 것이 바로 JVM이죠. 이때 JVM은 OS로부터 프로그램 실행에 필요한 메모리를 할당받는데, 이 영역을 Runtime Data Area라고 부릅니다.
메모리 구조를 알아야 하는 이유는 명확합니다.
- 메모리 누수(Memory Leak) 방지: 불필요한 객체가 쌓이는 원인을 알 수 있습니다.
- 성능 최적화: GC(Garbage Collection)가 언제, 어디서 발생하는지 이해할 수 있습니다.
- 에러 해결:
StackOverflowError와OutOfMemoryError의 원인을 정확히 파악할 수 있습니다.
Runtime Data Area의 전체적인 그림
JVM의 메모리 영역은 크게 Thread-Safe한 영역(공유 영역)과 Thread-Specific한 영역(개별 영역)으로 나뉩니다. 이 구분은 멀티 스레드 환경에서 동시성 이슈를 이해하는 데 아주 중요합니다.
모든 스레드가 공유하는 영역 (GC의 대상)
- Heap Area (힙 영역):
new키워드로 생성된 객체와 배열이 저장되는 곳. - Method Area (메서드 영역 / Metaspace): 클래스 정보, 상수, static 변수 등이 저장되는 곳.
스레드마다 별도로 생성되는 영역
- Stack Area (스택 영역): 메서드 호출 시 지역 변수, 매개변수, 리턴 값 등이 저장되는 곳.
- PC Register: 현재 실행 중인 JVM 명령의 주소를 가리킴.
- Native Method Stack: 자바 외의 언어(C, C++ 등)로 작성된 네이티브 코드를 위한 스택.
이제 가장 중요한 Metaspace, Heap, Stack 세 가지를 집중적으로 뜯어보겠습니다.
Metaspace (구 Method Area): 클래스의 청사진 보관소
Java 8 이전에는 PermGen(Permanent Generation)이라는 이름으로 불렸던 영역입니다. 저도 예전 자료를 보다가 PermGen과 Metaspace가 혼용되어 있어 헷갈렸던 기억이 나는데요.
Java 8, 무엇이 바뀌었나?
Java 7까지는 PermGen 영역이 Heap의 일부처럼 관리되었고, 크기가 제한적(기본 64MB 등)이어서 클래스를 많이 로딩하는 프레임워크를 쓰면 java.lang.OutOfMemoryError: PermGen space 에러가 자주 발생했습니다.
이를 해결하기 위해 Java 8부터는 PermGen이 사라지고 Metaspace가 도입되었습니다.
- 위치 변경: JVM 메모리(Heap)가 아닌 Native Memory(OS 레벨의 메모리)를 사용합니다.
- 특징: 개발자가 크기를 제한하지 않으면 OS가 허용하는 한 유동적으로 크기가 늘어납니다. 즉, 메모리 부족 오류로부터 훨씬 자유로워졌죠.
Metaspace에 저장되는 것들
이곳은 "붕어빵 틀"이 저장되는 곳이라고 이해하면 쉽습니다.
- Class Metadata: 클래스 이름, 접근 제어자, 필드 정보, 메서드 정보 등.
- Static Variables:
static키워드가 붙은 변수들. (단, Java 7 이후부터 String Constant Pool과 정적 변수 자체는 Heap으로 이동했다는 설도 있지만, 기본적으로 클래스 수준의 정보는 이곳에 메타데이터로 남습니다.) - Runtime Constant Pool: 상수들이 저장되는 공간.
Stack Area: 메서드들의 작업 공간
Stack은 "잠깐 사용하고 버리는 일회용 작업대"와 같습니다. LIFO(Last In First Out) 구조로 작동하며, 메서드가 호출될 때 할당되고 종료되면 즉시 소멸합니다.
Stack Frame (스택 프레임)
메서드가 하나 호출될 때마다 스택에는 Stack Frame이라는 블록이 하나씩 쌓입니다. 이 프레임 안에는 다음 정보들이 담깁니다.
- Local Variables Array: 메서드 안의 지역 변수, 매개변수.
- Operand Stack: 연산을 위한 임시 공간 (예:
a + b를 할 때 값을 잠시 두는 곳). - Frame Data: 상수 풀 참조 정보, 리턴 주소 등.
Primitive vs Reference
여기서 정말 중요한 개념이 나옵니다.
- 기본형(Primitive Type) 변수:
int,double,boolean등은 실제 값(Value) 자체가 스택에 저장됩니다. - 참조형(Reference Type) 변수:
String,Integer, 사용자 정의 객체 등은 객체의 주소 값(Reference)만 스택에 저장되고, 실제 객체 데이터는 Heap에 존재합니다.
⚠️ 주의!
재귀 함수를 잘못 짜서 메서드가 무한히 호출되면, 스택 프레임이 계속 쌓이다가 공간이 부족해져 그 유명한 StackOverflowError가 발생합니다.
Heap Area: 객체들의 거대한 창고
대망의 Heap 영역입니다. JVM 메모리 중 가장 크고 복잡하며, Garbage Collector(GC)가 활동하는 주 무대입니다. 프로그램 상에서 데이터를 저장하기 위해 런타임에 동적으로 할당되는 메모리 영역이죠.
Heap의 구조 (HotSpot JVM 기준)
효율적인 GC를 위해 Heap은 다시 물리적으로 나뉘어 있습니다. (G1 GC 등 최신 GC에서는 논리적 구분이 달라지지만, 기본 개념은 같습니다.)
1) Young Generation (신입생 구역)
- 새롭게 생성된 객체(New)가 할당되는 곳입니다.
- 대부분의 객체는 생성되자마자 금방 사용되지 않게 되기 때문에(Unreachable), 이곳에서 생성되었다가 금방 사라집니다.
- 이곳에서 일어나는 GC를 Minor GC라고 합니다. 빠르고 가볍습니다.
- Eden: 객체가 최초로 생성되는 공간.
- Survivor 0 / 1: Eden에서 살아남은 객체들이 잠시 대피하는 공간.
2) Old Generation (졸업생 구역)
- Young 영역에서 오랫동안 살아남은(GC를 여러 번 견딘) 객체들이 이동(Promotion)해 오는 곳입니다.
- 크기가 크며, 이곳이 가득 차면 Major GC (Full GC)가 발생합니다.
- Full GC가 발생하면 JVM은 애플리케이션 실행을 멈추는데(Stop-The-World), 이게 서버 렉의 주범이 되기도 합니다.
코드 예제로 보는 메모리 흐름 (시뮬레이션)
백문이 불여일견! 간단한 코드를 통해 변수들이 어디에 꽂히는지 따라가 봅시다.
public class MemoryTest {
public static void main(String[] args) { // 1. main 스택 프레임 생성
int age = 30; // 2. Stack에 저장 (값: 30)
String name = "Java"; // 3. Stack에 참조변수 name, Heap(String Pool)에 "Java" 객체
User user = new User("Kim"); // 4. Stack에 참조변수 user, Heap에 User 객체 생성
user.study(); // 5. study() 스택 프레임 생성 -> 종료 후 소멸
} // 6. main 종료 -> 모든 스택 프레임 및 변수 제거
}
class User {
String name; // Heap (User 객체 내부)
public User(String name) {
this.name = name;
}
public void study() {
int hour = 2; // Stack (study 메서드 프레임 내부)
System.out.println(name + " is studying for " + hour + " hours.");
}
}
단계별 분석
main()실행: 스택 영역에main()을 위한 스택 프레임이 생성됩니다.int age = 30: 기본형이므로 스택 프레임 내부에 변수age와 값30이 바로 저장됩니다.User user = new User("Kim"):new가 실행되었으므로 Heap 영역의 Eden 영역에User객체가 생성됩니다.- 이
User객체의 메모리 주소값(참조값)이 Stack 영역의user변수에 저장됩니다. - 즉, Stack(
user)이 Heap(User 객체)을 가리키고 있는 형태입니다.
user.study():study()메서드를 위한 새로운 스택 프레임이main()프레임 위에 쌓입니다.- 내부 변수
hour는study()프레임 안에 저장됩니다. - 메서드가 끝나면
study()프레임은 스택에서 사라지고(pop),hour변수도 함께 메모리에서 증발합니다.
main()종료:- 마지막 남은
main()프레임도 사라집니다. - Stack이 비워지면서 Heap에 있는
User객체를 참조하는 변수가 아무도 없게 됩니다(Unreachable). - 이제 이
User객체는 나중에 GC가 올 때 수거해 갑니다.
- 마지막 남은
우리가 이 구조를 알아야 하는 진짜 이유
처음 Java를 배울 때는 문법 익히기에 바빠 메모리까지 신경 쓸 겨를이 없습니다. 하지만 실무에서 대용량 데이터를 처리하거나, 오랫동안 실행되는 데몬 서버를 만들다 보면 메모리 관리는 선택이 아닌 필수가 됩니다.
오늘의 핵심 요약
- Static(
static): Metaspace에 저장되어 프로그램 종료 시까지 살아있다. (남발하면 메모리 낭비!) - 지역변수(Local Variable): Stack에 저장되어 메서드 종료 시 칼같이 사라진다.
- 객체(Object): Heap에 저장되어 스택의 참조변수가 놓아줄 때까지(Unreachable) 살아있다가 GC에 의해 청소된다.
특히 최근에는 클라우드 환경(AWS, Docker, Kubernetes)에서 컨테이너 기반으로 배포하는 경우가 많은데, 이때 컨테이너의 메모리 제한과 JVM의 Heap 설정을 올바르게 매핑하는 것이 정말 중요합니다.
Java Concurrent 컬렉션과 병렬 스트림의 함정, 제대로 알고 사용하자
멀티스레드 환경에서 안전한 데이터 공유는 Java 개발자들이 피할 수 없는 과제다. 특히 Java 8부터 도입된 스트림과 함께 Concurrent 컬렉션을 사용할 때, 많은 개발자들이 같은 실수를 반복한다. 우
byteandbit.tistory.com
'JAVA' 카테고리의 다른 글
| Java GC 기초부터 로그 분석까지: 세대별 GC와 마크-스윕 완벽 이해하기 (0) | 2025.12.13 |
|---|---|
| Java 클래스 로딩부터 실행까지: 바이트코드와 ClassLoader의 모든 것 (0) | 2025.12.09 |
| Java Concurrent 컬렉션과 병렬 스트림의 함정, 제대로 알고 사용하자 (0) | 2025.12.07 |
| Java 멀티스레드의 악몽, 데드락·라이블락·스타베이션 완벽 정리 (0) | 2025.12.06 |
| Java Executors, Future, Callable의 개념과 활용법을 완벽히 이해하자 (0) | 2025.11.23 |