자바 메모리 구조(기초) — Stack / Heap / Method Area

2025. 9. 22. 23:19·자바

자바 메모리 구조(기초) — Stack / Heap / Method Area

자바 프로그램이 어떻게 메모리를 쓰는지 이해하면, 디버깅·성능·안정성까지 한층 수월해 진다. 이 글에서는 JVM이 사용하는 세 가지 핵심 영역 스택(Stack), 힙(Heap), 메서드 영역(Method Area, JDK 8+에서는 Metaspace) 을 정리한다.


왜 메모리 구조를 알아야 할까

  • 버그 원인 파악: NullPointerException, StackOverflowError, 메모리 누수 등 문제의 뿌리를 이해하게 도니다.

  • 성능 최적화: 객체 생성/소멸 비용과 GC의 동작 맥락을 파악해 불필요한 할당을 줄일 수 있다.

  • 설계 품질 향상: 불변 객체, 값/참조语 의미, 라이프사이클을 고려한 API 설계가 가능해진다.


JVM 메모리 큰 그림

  • 스택(Stack): 각 스레드마다 존재. 메서드 호출 시 생기는 스택 프레임에 지역 변수, 매개변수, 리턴주소 등이 저장된다. 범위를 벗어나면 즉시 사라진다.

  • 힙(Heap): new 로 생성한 모든 객체가 살아가는 공간. 가비지 컬렉터(GC)가 더 이상 참조되지 않는 객체를 회수한다.

  • 메서드 영역(Method Area / Metaspace): 클래스 메타데이터(필드/메서드 시그니처, 상수 풀 등), static 필드, 바이트코드 등의 정보를 저장한다. JDK 8부터는 PermGen이 제거되고 네이티브 메모리를 쓰는 Metaspace로 대체되었다.


1) 스택(Stack) — “메서드의 작업대”

핵심 개념

  • 스택은 스레드별로 하나씩 존재한다. 스레드가 많을수록 스택도 많아진다.

  • 메서드를 호출할 때마다 스택 프레임이 쌓였다가, 메서드가 끝나면 해당 프레임이 즉시 제거된다.

  • 지역 변수(기본형 값, 객체에 대한 참조값)와 매개변수는 스택 프레임 안에 위치한다.

예제: 스택 프레임과 지역 변수

public class StackDemo {
    public static void main(String[] args) {
        int a = 10;                 // 스택(main 프레임)
        int b = add(a, 5);          // add 프레임 생성
        System.out.println(b);
    }
    static int add(int x, int y) {  // 스택(add 프레임)
        int sum = x + y;            // 지역 변수 sum
        return sum;                 // 프레임 제거 후 main으로 복귀
    }
}

자주 하는 오해

  • “기본형은 항상 스택”: 지역 변수일 때만 그렇다. 객체의 필드로 존재하는 기본형은 힙의 객체 내부에 저장된다.

  • 재귀 호출 깊이가 너무 깊으면 StackOverflowError 가 난다다.


2) 힙(Heap) — “객체들이 살아가는 곳”

핵심 개념

  • new 로 생성한 모든 객체와 배열은 힙에 올라간다.

  • 변수에는 객체 자체가 아니라 객체를 가리키는 참조값이 담긴다.

  • 더 이상 참조되지 않는 객체는 GC가 회수한다. 회수 시점은 JVM의 판단에 따르며 즉각적이지 않을 수 있다.

예제: 참조 전달과 객체 상태 변경

class Box { int v; }

public class HeapDemo {
    static void reassign(Box b) { b = new Box(); b.v = 99; } // 참조 재할당(원본 영향 없음)
    static void mutate(Box b)   { b.v = 99; }                // 같은 객체의 상태 변경(원본 영향 있음)

    public static void main(String[] args) {
        Box box = new Box(); // 힙에 Box 객체 생성
        box.v = 1;

        reassign(box);
        System.out.println(box.v); // 1 (원본 참조는 그대로)

        mutate(box);
        System.out.println(box.v); // 99 (같은 객체의 상태가 변함)
    }
}
  • 자바는 항상 값에 의한 호출(pass-by-value) 이다. 참조형 인자의 경우 “참조값의 복사본”이 전달된다. 메서드 안에서 참조 재할당은 원본에 영향을 주지 않지만, 객체 상태 변경은 영향을 준다.

한 걸음 더

  • HotSpot JVM은 최적화 시 Escape Analysis로 “절대 외부로 새지 않는(short-lived) 객체”를 스택에 올리거나(Scalar Replacement) 할당 자체를 제거하기도 한다. 개념적으로는 “객체는 힙”이 맞지만, 최적화로 예외가 생길 수 있다는 점만 참고하자.

3) 메서드 영역(Method Area / Metaspace) — “클래스의 설계도 창고”

핵심 개념

  • 클래스 메타데이터(이름, 상속, 필드/메서드 시그니처), 런타임 상수 풀(Constant Pool), static 필드, 바이트코드 등이 저장된다.

  • JDK 8부터는 메서드 영역 구현이 Metaspace로 바뀌었습니다(네이티브 메모리 사용). 과거 PermGen의 고정 크기 문제(자주 나는 OOME)를 해결하기 위함이다.

  • 클래스 로딩 수명은 클래스로더의 수명과 연동됩니다. 특정 클래스로더가 더 이상 reachable 하지 않으면 클래스 언로드가 가능해진다.

예제: static 과 인스턴스의 차이

class Counter {
    static int total; // 메서드 영역(정확히는 클래스 메타데이터의 static 저장 위치)에서 관리
    int value;        // 각 인스턴스마다 힙에 저장

    Counter() { total++; }
}

public class MethodAreaDemo {
    public static void main(String[] args) {
        Counter c1 = new Counter(); // total=1
        Counter c2 = new Counter(); // total=2
        System.out.println(Counter.total); // 2
        System.out.println(c1.value);      // 0
        System.out.println(c2.value);      // 0
    }
}

문자열 상수와 상수 풀

public class StringPoolDemo {
    public static void main(String[] args) {
        String s1 = "Java";
        String s2 = "Java";
        String s3 = new String("Java");

        System.out.println(s1 == s2); // true (리터럴은 풀 공유)
        System.out.println(s1 == s3); // false (new는 힙에 새 객체)
    }
}
  • 문자열 리터럴은 런타임 상수 풀을 통해 공유된다.

  • == 는 참조 동일성, equals 는 내용 동등성 비교이다.


객체가 만들어지고 사라지기까지 — 라이프사이클 정리

  1. 클래스 로딩: 클래스로더가 .class 바이트코드를 읽어들인다.

  2. 링킹(검증→준비→해결): 형식 검증, static 필드 메모리 할당·기본값 설정, 심볼릭 참조 해소 등을 수행한다.

  3. 초기화(Initialization): static 초기화 블록과 static 필드 초기화가 실행된다.

  4. 인스턴스 생성:

    • 힙에 메모리 할당 → 0으로 초기화

    • 인스턴스 필드 초기화 → 생성자 호출

  5. 사용 및 소멸: 더 이상 참조되지 않으면 GC 후보가 되고, 수거될 수 있다.


자주 하는 오해와 정리

  • 기본형은 “항상 스택”이 아니다. 필드로 있으면 힙의 객체 내부에 저장된다.

  • GC는 “즉시” 동작하지 않는다. JVM 정책과 시점에 따라 돌며, 컬렉터 종류에 따라 특성이 다르다.

  • == 와 equals 는 다르다. 참조 동일성과 값 동등성을 구분하자.

  • 스택은 스레드별로 독립적이다. 멀티스레드 환경에서 스택 간 공유는 없다. 공유는 힙 객체를 통해 이뤄진다.

  • 메서드 영역은 JDK 8+에서 Metaspace로 구현된다. 클래스가 많거나 동적 생성이 잦다면 Metaspace 사이즈 설정을 고려하자.


요약

  • 스택: 메서드 호출 단위의 지역 저장소. 프레임이 쌓였다가 즉시 사라진다.

  • 힙: new 로 만든 모든 객체의 집. GC가 관리한다.

  • 메서드 영역/Metaspace: 클래스 메타데이터, static, 상수 풀, 바이트코드를 담는 공간.

저작자표시 비영리 (새창열림)

'자바' 카테고리의 다른 글

Java Class 하위에 변수를 몇개까지 선언할 수 있을까?  (0) 2025.11.11
[JVM 완전정복 #1] JVM 구성요소 총정리: 가상 머신이 자바 코드를 실행하는 비밀  (0) 2025.11.11
프로그래밍 언어의 시작 – 고급어와 저급어의 이해  (0) 2025.11.04
객체지향 프로그래밍(Object-Oriented Programming)  (1) 2025.10.28
프로그래밍이란 무엇일까?  (0) 2025.10.23
'자바' 카테고리의 다른 글
  • [JVM 완전정복 #1] JVM 구성요소 총정리: 가상 머신이 자바 코드를 실행하는 비밀
  • 프로그래밍 언어의 시작 – 고급어와 저급어의 이해
  • 객체지향 프로그래밍(Object-Oriented Programming)
  • 프로그래밍이란 무엇일까?
깊은바다속꼬북이
깊은바다속꼬북이
  • 깊은바다속꼬북이
    CodeBlossom
    깊은바다속꼬북이
  • 전체
    오늘
    어제
    • 분류 전체보기 (53) N
      • 라이징 캠프 (4)
      • 객채지향 개발론 (3)
      • 스프링 (10) N
      • 네트워크 (2)
      • 자바 (16)
      • 자료구조 (3)
      • 운영체제 (0)
      • 데이터베이스 (4)
      • 디자인패턴 (7)
      • JSP (1)
      • 개발 알쓸신잡 (2)
      • 일반 교양 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    GC
    트랜잭션 전파레벨
    jvm 클래스 로더
    자료구조
    디자인패턴
    어댑터 패턴(Adapter Pattern)
    JUnnit5
    객체지향
    자바 Socket 클래스
    싱글턴 패턴(Singleton Pattern)
    junnit5프레임워크
    MySQL 옵티마이저
    spring
    한국어 검색
    개발자 철학
    java data area
    MySQL 파서
    JVM
    프로그램밍 언어
    백엔드
    mockito라이브러리
    스프링
    개발 교훈
    java 버전별 특징
    개발 철학
    jit-compiler
    디자인 패턴
    템플릿 메서드 패턴(Template Method Pattern)
    전략 패턴(Strategy Pattern)
    java
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
깊은바다속꼬북이
자바 메모리 구조(기초) — Stack / Heap / Method Area
상단으로

티스토리툴바