Java 스택 vs 힙: 메모리 할당 실전 가이드
스택 메모리란?
Java에서 스택은 메서드 호출 프레임, 로컬 변수, 참조 변수를 보관하는 연속된 메모리 영역입니다. 메서드가 호출될 때마다 새로운 프레임이 스택에 푸시됩니다. 스택 메모리는 후입선출(LIFO) 방식으로 구성되어, 가장 최근 호출이 메서드 반환 시 가장 먼저 제거됩니다.
힙 메모리란?
힙은 Java 객체가 생성되는 영역입니다. 모든 클래스 인스턴스와, 선택적으로 해당 객체를 가리키는 참조 변수를 보관합니다. 스택과 달리 힙은 단일 메서드 호출의 생명주기에 얽매이지 않아, 객체가 생성된 스코프를 벗어나도 지속될 수 있습니다.
Java 메모리 할당
Java 메모리 할당은 프로그램 데이터를 위해 가상 메모리를 예약하는 과정을 말합니다. 변수는 선언 시 할당되지 않으며, 대신 참조가 생성됩니다. new 연산자를 사용할 때 JVM이 객체를 위해 힙에 공간을 할당합니다.
Java는 효율적인 관리를 위해 메모리를 네 가지 영역으로 나눕니다:
- 힙(Heap)
- 스택(Stack)
- 코드(Code/Bytecode)
- 정적(Static/클래스 데이터)
- 코드 영역에는 컴파일된 바이트코드가 들어있습니다.
- 스택에는 메서드 프레임, 로컬 변수, 참조 변수가 저장됩니다.
- 힙에는 객체와, 가끔 참조 변수가 저장됩니다.
- 정적 영역에는 정적 필드와 메서드가 보관됩니다.
로컬 변수 vs 인스턴스 변수
인스턴스 변수는 클래스 내부에 선언되되 메서드 밖입니다:
class Student {
int num; // 인스턴스 변수
public void showData() {}
}
로컬 변수는 메서드 내부(또는 메서드 매개변수)에 선언됩니다:
public void sum(int a) {
int x = a + 3; // a와 x는 로컬 변수
}
스택과 힙의 상호작용
메서드 호출 중 스택과 힙이 어떻게 상호작용하는지 예제로 살펴보겠습니다.
main 메서드가 m1()을 호출한다고 가정:
public void m1() {
int x = 20;
}
m1용 프레임이 스택에 푸시되고, 로컬 변수 x가 이 프레임에 저장됩니다.
m1이 m2(int b)를 호출하면 m1 프레임 위에 새 프레임이 추가됩니다:
public void m2(int b) {
boolean c;
}
마찬가지로 m2가 m3()를 호출해 또 다른 프레임 생성:
m3 내부에서 Account 객체가 힙에 인스턴스화되고, 그 참조 ref는 스택에 존재합니다:
class Account {
int p;
int q;
}
public void m3() {
Account ref = new Account(); // 추가 코드
}
참조 변수 ref는 m3의 스택 프레임에 생성되어 힙에 할당된 Account 객체를 가리킵니다.
m3가 완료되면 스택 프레임이 팝되고, Account 객체는 참조가 없어져 가비지 컬렉션 대상이 됩니다.
m2, m1도 같은 과정을 거쳐 결국 main으로 제어가 돌아옵니다.
인스턴스 변수로서의 객체 참조
B 인스턴스에 대한 참조를 가진 클래스 A를 생각해 보세요:
public static void main(String[] args) {
A parent = new A(); // 추가 코드
}
class A {
B child = new B();
int e;
}
class B {
int c;
int d;
}
여기서 child 참조는 A 객체의 일부로 힙에 저장되며, 자신의 B 인스턴스를 가리킵니다.
핵심 요약
- 메서드 호출마다 스택에 새 프레임 생성
- 로컬 변수는 스택에, 인스턴스 변수는 힙에 저장
- 참조 변수는 스택에 저장되지만 힙 객체를 가리킴
- 스택 프레임이 팝되면 로컬/참조 변수는 버려지고, 고아 힙 객체는 GC 대상이 됨