본문 바로가기
platform_infra_cloud/java_runtime

JVM 메모리 구조 상세 분석 – Heap, Stack, Metaspace의 동작 원리

by 죄니안죄니 2025. 4. 19.

들어가며

 

Java 애플리케이션이 실행되면 JVM(Java Virtual Machine) 이 메모리를 어떻게 사용하느냐는 성능과 안정성에 큰 영향을 줍니다.

특히 Heap, Stack, Metaspace는 각각의 역할이 명확하며, 개발자가 메모리 구조를 이해하고 있어야 메모리 누수, GC 문제 등을 예방할 수 있습니다.

이 글에서는 JVM 메모리 구조를 실무 중심으로 구조별 역할, 메모리 할당 위치, GC 관점에서의 차이점까지 상세하게 정리합니다.


1. JVM 메모리 영역 개요

┌────────────────────┐
│     메서드 영역     │ → Metaspace (Java 8+)
├────────────────────┤
│     힙 영역 (Heap) │ ← 객체 저장소, GC 대상
├────────────────────┤
│   스택 영역 (Stack) │ ← 메서드 호출, 지역 변수
├────────────────────┤
│ 네이티브 메모리 영역│ ← JNI, 클래스 로더
└────────────────────┘

메모리 구조는 자바 버전에 따라 변동이 있습니다. (PermGen → Metaspace)


2. Heap – 객체가 저장되는 공간

✅ 특징

  • new 키워드로 생성한 객체는 모두 Heap에 저장
  • GC(Garbage Collector)의 주요 대상
  • 크기가 크고 애플리케이션 전반에 공유됨 (힙에 저장된 객체는 다른 메서드, 스레드에서도 참조 가능)
String name = new String("John"); // name → Stack, 객체 → Heap

✅ 구조 세분화

영역  설명
Young Generation 새로 생성된 객체들이 위치 (Eden + Survivor)
Old Generation 장기간 생존한 객체들이 이동됨 (Tenured)

GC는 주로 Young 영역에서 자주 발생하고, Old 영역은 Full GC 시 대상이 됩니다.


3. Stack – 메서드 호출과 지역 변수 저장소

✅ 특징

  • 각 스레드마다 독립된 Stack 공간을 가짐→ 즉, 여러 스레드가 동시에 실행되어도 자신의 메서드 호출 정보는 자신만의 스택에 저장됨
  • 메서드 호출 시마다 스택 프레임이 생성됨→ 지역 변수, 매개변수, return 주소 등이 저장됨
  • 메서드 종료 시 자동 제거 (GC 대상 아님)
💡 **프로그램이 시작되면 기본적으로 하나의 메인 스레드(main thread)**가 생성되고, 그에 대한 Stack도 함께 할당됩니다.
이후 개발자가 Thread를 추가로 생성하면 각각의 스레드는 자신만의 Stack 공간을 가집니다.
public void greet() {
    String msg = "Hello"; // msg 참조 변수는 Stack, "Hello" 객체는 Heap
    int age = 20;          // 기본형 age 값은 Stack에 직접 저장
}

스택은 속도는 빠르지만 크기가 제한적이라 StackOverflowError가 발생할 수 있음

✅ 기본형 vs 참조형 저장 위치 요약

타입 구분 Stack 저장 Heap 저장
기본형(int, boolean 등) ✅ 실제 값 저장 ❌ 없음
참조형(String, 객체 등) ✅ 참조 주소 저장 ✅ 객체 본체 저장

변수명은 컴파일 타임까지만 존재하고, 런타임에는 사라집니다.

즉:

  • int a = 10;에서 a는 개발자가 코드를 이해하기 위한 이름
  • 컴파일되면 JVM 내부에서는 메서드의 스택 프레임 안에 슬롯(slot) 번호로 치환됩니다

예를 들어:

int a = 10;
int b = 20;

→ 실제로는 스택 안에 0번, 1번 슬롯에 저장될 뿐이고 "a"나 "b"라는 이름은 JVM 런타임엔 존재하지 않음

📌 단, 디버깅 시 변수명을 볼 수 있는 이유는 .class에 심볼 테이블이 포함되었기 때문입니다 (옵션으로 생략 가능)

 

  • 자바 바이트코드 레벨에서는 지역 변수들이 번호로 된 슬롯(slot)에 저장됩니다.
  • 슬롯(slot): 변수명이 아니라 바이트코드에서 JVM이 관리하는 지역 변수의 순번 공간이라는 개념
  • 이 슬롯은 실제 물리적 메모리 주소는 아니고, JVM이 관리하는 논리적 번호 공간입니다.
  • 예:
int a = 10;       // → 슬롯 0번
String s = ...;   // → 슬롯 1번 (참조 주소)
  • 원시타입은 값 자체를, 참조타입은 주소를 슬롯에 저장한다
  • 즉, 변수명이 아닌 순번으로 변수를 관리하며, 컴파일 이후에는 a, s 같은 이름은 없어지고 슬롯 번호만 남습니다.

4. Metaspace – 클래스 메타 정보 저장소 (Java 8+)

✅ PermGen → Metaspace 변화

  • Java 7 이하: 메타 정보를 PermGen 영역에 저장 (크기 제한 있음)
  • Java 8 이후: OS의 네이티브 메모리를 사용하는 Metaspace로 대체

✅ Metaspace에 저장되는 것

  • 클래스 이름, 메서드/필드 정보, 어노테이션 등 클래스 로딩 관련 메타데이터

✅ 장점

  • PermGen보다 유연한 크기 조정 (OutOfMemoryError 감소)
  • XX:MaxMetaspaceSize로 최대 크기 지정 가능

네, 맞습니다. Metaspace는 자바(JVM)에서만 쓰는 메모리 영역 개념이에요.

  • C/C++ 같은 언어는 클래스 자체가 없으니 이런 개념도 없음
  • 자바에서는 클래스를 동적으로 로딩/해제할 수 있기 때문에, JVM은 이 클래스 메타데이터들을 따로 관리하는 공간이 필요해요 → 그것이 Metaspace입니다.

📌 저장되는 것:

  • 클래스 이름, 상속 관계, 메서드 목록, 필드 타입, 어노테이션 등 "클래스 자체에 대한 정보"

 

 

 


5. 예제 기반 메모리 저장 구조 시각화

public class Main {
    public static void main(String[] args) {
        int a = 10;                      // Stack (기본형)
        String s = new String("Hi");    // Stack(s), Heap("Hi")
        User u = new User("Tom");       // Stack(u), Heap(User 객체)
    }
}
변수명 타입 Stack 저장 내용 Heap 저장 내용
a 기본형 값 10 -
s 참조형 참조 주소 "Hi" 문자열 객체
u 참조형 참조 주소 new User("Tom") 객체

 


6. 기타 메모리 영역

🔸 코드 캐시(Code Cache)

  • JIT 컴파일된 네이티브 코드 저장 영역

🔸 Direct Memory

  • NIO(ByteBuffer.allocateDirect 등)에서 사용됨

🔸 Thread Stack

  • 각 스레드별로 Stack, PC Register 등 포함됨

6. GC 관점에서의 정리

메모리 영역  GC 대상 여부  사용 예  주의 사항
Heap ✅ 예 new 객체, 배열 GC 성능에 영향 큼
Stack ❌ 아니오 지역 변수 너무 깊은 호출 시 StackOverflowError
Metaspace ❌ 아니오 클래스 정보 클래스 로더 누수 시 OOM 가능

스택 구조는 메서드를 호출할 때마다 스택 프레임이 위에 쌓이는데요,

void a() { b(); }
void b() { c(); }
void c() { d(); }
...

→ 이런 식으로 너무 많이 메서드를 호출하면, 결국 스택 공간이 **한계(깊이)**에 도달하고 StackOverflowError가 발생합니다.

더 극단적인 예:

void recur() {
  recur(); // 자기 자신을 계속 호출
}

→ 스택이 무한히 쌓이다가 에러 납니다.

 

 

JVM은 클래스 로더가 로드한 클래스를 클래스 로더가 GC 대상이 될 때까지 해제하지 않음

누수가 발생하는 경우:

  • WebApp을 재배포하면 새로운 클래스 로더가 생성되는데,
  • 이전 클래스 로더가 정적 변수 또는 스레드 등에 의해 참조되고 있으면 GC 대상이 안 됨
  • 그 결과, 클래스 정보(Metaspace)가 계속 메모리에 남아 있음 → OutOfMemoryError 발생

예: Spring의 Bean, 정적 필드, ThreadLocal 등이 참조를 유지하고 있으면 문제가 됩니다.

 


7. 실무에서의 활용 팁

  • GC 튜닝 시 Heap 크기 조정: -Xms, -Xmx
  • Metaspace OOM 방지: XX:MaxMetaspaceSize=256m 등 명시
  • 스레드 수가 많아질 경우 Stack 메모리 확인: -Xss
  • 클래스 로더 캐시 누수 점검: Fat Jar, 리플렉션 등

JVM 옵션으로 실행 시점에 명령어로 설정합니다.

💻 사용 예

java -XX:MaxMetaspaceSize=256m -jar myapp.jar

//java 자바 애플리케이션을 실행하는 명령어
//-XX:MaxMetaspaceSize=256m Metaspace 최대 크기를 256MB로 제한
//-jar myapp.jar myapp.jar이라는 JAR 파일을 실행하라는 명령
//-XX:InitialMetaspaceSize=64m 시작 시 할당하는 metaspace 크기
//-XX:+PrintGCDetails GC 로그에서 metaspace 사용량 확인 가능
  • 또는 IDE (IntelliJ, Eclipse)의 Run/Debug Configuration > VM Options에 추가
  • 또는 JAVA_OPTS, JVM_OPTS 환경변수로 지정 가능

💡 설정 이유:

  • Metaspace는 기본적으로 OS 메모리를 무제한으로 쓰려 함
  • 서버에서는 메모리 누수 방지를 위해 적절히 제한하는 것이 중요

 


마치며

📂 language / java 카테고리에서는 다음 내용을 이어서 다룹니다:

  • JVM GC 동작 방식 (Minor GC, Major GC, Full GC)
  • GC 튜닝 전략과 도구 (jstat, jvisualvm)
  • 메모리 누수 감지와 실전 디버깅

JVM 메모리 구조는 성능을 결정짓는 핵심입니다.

Heap, Stack, Metaspace의 구조와 차이를 명확히 이해하면,

애플리케이션의 안정성과 확장성을 한 단계 높일 수 있습니다.

 

📌 이전 글 다시보기

👉 자바 플랫폼이란? – JVM, JDK, JRE로 이해하는 실행 환경의 구조

📌 다음 글 미리보기
👉 GC의 종류와 튜닝 전략 (G1GC, ZGC, ParallelGC)

📚 Java_runtime 시리즈 전체 보기
👉 https://jobreview.tistory.com/category/platform_infra_cloud/java_runtime

 

📌 바로보러가기 👉 자바 애플리케이션 구동과 메모리 로딩 과정  

댓글