반응형
자바의 스택 프레임과 변수의 생명 주기를 알아보자.
1. 스택 프레임
스택 프레임이란?
- 컴퓨터가 프로그램을 실행할 때, 메모리를 사용하는 방법 중 하나가 스택(stack)이다. 스택은 책을 쌓아 올리는 것처럼 데이터를 차곡차곡 쌓았다가, 마지막에 넣은 것부터 꺼내는 구조다.
- 지금부터 알아볼 스택 프레임은 메서드(함수)를 호출할 때마다 생성되는 작은 상자라고 생각하면 된다. 이 상자 안에는 그 메서드에서 사용하는 지역 변수와 매개변수가 들어 있다. 메서드가 끝나면 그 상자는 스택에서 사라지게 된다.
전체 메모리 구조 그림
- 스택 프레임을 이해하기 위해 먼저 자바의 메모리 구조를 알아보자.
[메모리 구조]
+--------------------------+
| Method Area | <-- static 변수
| (클래스, static 변수) |
+--------------------------+
| Heap | <-- 인스턴스 변수 (객체)
| (객체들이 저장됨) |
+--------------------------+
| Stack | <-- 스택 프레임
| [현재 실행 중인 메서드] |
| (지역 변수와 매개변수 저장) |
+--------------------------+
메모리 구조 자세히 보기
- Method Area (메서드 영역)
- 역할: 클래스 정보와 static 변수가 저장되는 공간이다.
- 예시: 클래스의 메서드나 static 변수는 이곳에 저장된다.
- 특징: 프로그램이 시작될 때 생성되고, 프로그램이 종료될 때까지 유지돼요.
- Heap (힙)
- 역할: 인스턴스 변수가 저장되는 공간이다. 즉, 생성된 객체(인스턴스)들이 이곳에 저장된다.
- 예시: new 키워드를 사용해 생성한 객체들이 힙에 저장된다.
- 특징: 객체는 힙에 저장되며, 객체가 더 이상 필요 없을 때 자바의 가비지 컬렉터가 자동으로 메모리를 정리해 준다.
- Stack (스택)
- 역할: 스택 프레임이 저장되는 공간이다. 현재 실행 중인 메서드와 그 메서드의 변수들이 여기에 저장된다.
- 예시: 메인 메서드나 다른 메서드를 호출할 때마다 새로운 스택 프레임이 생성된다.
- 특징: 메서드가 호출될 때마다 스택에 쌓이고, 메서드가 종료되면 스택에서 제거된다. 이곳은 아주 빠르게 접근할 수 있는 메모리 영역이다.
스택 프레임의 구성
- 스택 프레임은 크게 세 부분으로 나눌 수 있다.
- 매개변수(Parameter): 메서드가 호출될 때 전달되는 값들이다.
- 지역 변수(Local Variables): 메서드 내부에서 선언된 변수들이다.
- 반환 주소(Return Address): 메서드가 끝난 후 돌아갈 위치를 저장한다.
스택 프레임 그림
[Stack]
+--------------------------+
| [methodA 스택 프레임] |
| - paramVar = 5 |
| - localVar = 10 |
| - 반환 주소 |
+--------------------------+
| [main 스택 프레임] |
| - args |
| - obj |
| - 반환 주소 |
+--------------------------+
2. 변수의 종류와 생명 주기
1. 지역 변수
- 정의: 메서드 안에서 선언된 변수다.
- 생명 주기: 메서드가 호출될 때 생성되고, 메서드가 끝나면 사라진다.
- 예: int a = 10;
public class LocalVariableExample {
// 메서드 호출 시 새로운 스택 프레임 생성
public void displayNumber() {
int a = 10; // 지역 변수 'a' 선언 및 초기화
System.out.println("지역 변수 a의 값: " + a);
}
public static void main(String[] args) {
LocalVariableExample example = new LocalVariableExample();
example.displayNumber(); // displayNumber 메서드 호출
}
}
- displayNumber 메서드 안에 int a = 10;이라는 지역 변수가 선언되어 있다. 이 지역변수 a는 displayNumber 메서드가 호출될 때 생성되고, 메서드가 끝나면 사라진다.
2. 매개변수
- 정의: 메서드가 호출될 때 전달되는 값들이다.
- 생명 주기: 메서드가 호출될 때 생성되고, 메서드가 끝나면 사라진다. (매개변수는 지역변수의 한 종류다.)
- 예: void add(int x, int y)
public class ParameterExample {
// 매개변수 x와 y 선언
public void add(int x, int y) {
int sum = x + y; // 지역 변수 sum
System.out.println("매개변수 x와 y의 합: " + sum);
}
public static void main(String[] args) {
ParameterExample example = new ParameterExample();
example.add(5, 7); // add 메서드 호출 시 매개변수 전달
}
}
- add 메서드는 두 개의 매개변수 x와 y를 받는다. x와 y는 add 메서드가 호출될 때 생성되고, 메서드가 끝나면 사라진다.
3. 인스턴스 변수
- 정의: 클래스 안에 선언된 변수로, 객체가 생성될 때마다 만들어진다.
- 생명 주기: 객체가 생성될 때 생성되고, 객체가 더 이상 사용되지 않을 때 사라진다.
- 예: class Person { String name; }
public class Person {
String name; // 인스턴스 변수 'name' 선언
public Person(String name) { // 생성자에서 인스턴스 변수 초기화
this.name = name;
}
public void displayName() {
System.out.println("사람의 이름은: " + name);
}
public static void main(String[] args) {
Person person1 = new Person("철수"); // 객체 생성 시 인스턴스 변수 생성
Person person2 = new Person("영희"); // 또 다른 객체 생성
person1.displayName(); // 철수의 이름 출력
person2.displayName(); // 영희의 이름 출력
}
}
- Person 클래스 내부에는 String name이라는 인스턴스 변수가 선언되어 있다. Person 객체가 생성될 때마다 name 변수는 초기화된다. (Person 인스턴스(생성된 객체)가 GC 대상이 되면 인스턴스 변수(name)도 같이 사라진다.)
4. static 변수
- 정의: 클래스에 하나만 존재하는 변수다.
- 생명 주기: 프로그램이 시작될 때 생성되고, 프로그램이 끝날 때 사라진다.
- 예: static int count = 0;
public class StaticVariableExample {
static int count = 0; // static 변수 'count' 선언 및 초기화
public StaticVariableExample() { // 생성자에서 static 변수 증가
count++;
}
public void displayCount() {
System.out.println("현재 객체의 수: " + count);
}
public static void main(String[] args) {
StaticVariableExample obj1 = new StaticVariableExample(); // count = 1
obj1.displayCount();
StaticVariableExample obj2 = new StaticVariableExample(); // count = 2
obj2.displayCount();
StaticVariableExample obj3 = new StaticVariableExample(); // count = 3
obj3.displayCount();
}
}
- StaticVariableExample 클래스에는 static int count라는 static 변수가 선언되어 있다. StaticVariableExample 객체가 생성될 때마다 생성자에서 count를 1씩 증가시키도록 선언했다.
- 모든 객체가 같은 count 변수(static 변수)를 공유하기 때문에, 총 객체 수가 누적된다.
3. 변수의 생명 주기와 스택 프레임의 변화과정
예제 코드로 확인해 보기
- 변수의 생명 주기와 스택 프레임의 변화과정을 코드를 통해 알아보자.
public class VariableLifecycle {
static int staticVar = 100; // static 변수
int instanceVar = 200; // 인스턴스 변수
public static void main(String[] args) {
VariableLifecycle obj = new VariableLifecycle();
obj.methodA(5);
}
void methodA(int paramVar) { // 매개변수
int localVar = 10; // 지역 변수
System.out.println("메서드 A 시작");
System.out.println("staticVar: " + staticVar);
System.out.println("instanceVar: " + instanceVar);
System.out.println("paramVar: " + paramVar);
System.out.println("localVar: " + localVar);
methodB();
System.out.println("메서드 A 끝");
}
void methodB() {
int localVarB = 20; // 지역 변수
System.out.println("메서드 B 시작");
System.out.println("staticVar: " + staticVar);
System.out.println("instanceVar: " + instanceVar);
System.out.println("localVarB: " + localVarB);
System.out.println("메서드 B 끝");
}
}
1. 프로그램 시작
- 정적 변수인 staticVar는 프로그램이 시작될 때 Method Area에 올라간다. 그리고 main 메서드가 호출되면서 스택 프레임이 생성된다.
그림: 프로그램 시작 시 메모리 상태
[Method Area]
+----------------------+
| staticVar = 100 |
+----------------------+
[Heap]
(비어 있음)
[Stack]
+----------------------+
| [main 스택 프레임] |
| - args |
+----------------------+
2. 객체 생성
- VariableLifecycle obj = new VariableLifecycle();를 통해 인스턴스 변수인 instanceVar가 Heap 영역에 저장된다. 또한 생성된 obj 인스턴스의 주소 참조값은 main 스택 프레임에 저장된다.
그림: 객체 생성 후 메모리 상태
[Method Area]
+----------------------+
| staticVar = 100 |
+----------------------+
[Heap]
+----------------------+
| obj |
| - instanceVar = 200 |
+----------------------+
[Stack]
+----------------------+
| [main 스택 프레임] |
| - args |
| - obj (Heap의 주소) |
+----------------------+
3. main함수에서 methodA 호출
- obj.methodA(5);를 호출하면 methodA()에 대한 새로운 스택 프레임이 생긴다. 이때 paramVar(매개변수)와 localVar(지역 변수)가 스택 프레임에 저장된다.
public class VariableLifecycle {
static int staticVar = 100; // static 변수
int instanceVar = 200; // 인스턴스 변수
public static void main(String[] args) {
VariableLifecycle obj = new VariableLifecycle();
obj.methodA(5);
}
void methodA(int paramVar) { // 매개변수
int localVar = 10; // 지역 변수
System.out.println("메서드 A 시작");
System.out.println("staticVar: " + staticVar);
System.out.println("instanceVar: " + instanceVar);
System.out.println("paramVar: " + paramVar);
System.out.println("localVar: " + localVar);
methodB();
System.out.println("메서드 A 끝");
}
}
그림: methodA 호출 시 메모리 상태
[Method Area]
+----------------------+
| staticVar = 100 |
+----------------------+
[Heap]
+----------------------+
| obj |
| - instanceVar = 200 |
+----------------------+
[Stack]
+----------------------+
| [methodA 스택 프레임] |
| - paramVar = 5 |
| - localVar = 10 |
+----------------------+
| [main 스택 프레임] |
| - args |
| - obj |
+----------------------+
4. methodA 실행
- methodA가 실행되면서 staticVar, instanceVar, paramVar, localVar 값을 출력한다. 이 시점에서는 메모리 상태가 유지된다.
그림: methodA 실행 중
(위의 스택 상태 유지)
5. methodA 내부에서 methodB 호출
- methodB();를 호출하면 또 다른 스택 프레임이 생성된다. 이 스택 프레임에는 localVarB(지역 변수)가 저장된다.
public class VariableLifecycle {
static int staticVar = 100; // static 변수
int instanceVar = 200; // 인스턴스 변수
public static void main(String[] args) {
VariableLifecycle obj = new VariableLifecycle();
obj.methodA(5);
}
void methodA(int paramVar) { // 매개변수
int localVar = 10; // 지역 변수
System.out.println("메서드 A 시작");
System.out.println("staticVar: " + staticVar);
System.out.println("instanceVar: " + instanceVar);
System.out.println("paramVar: " + paramVar);
System.out.println("localVar: " + localVar);
methodB();
System.out.println("메서드 A 끝");
}
void methodB() {
int localVarB = 20; // 지역 변수
System.out.println("메서드 B 시작");
System.out.println("staticVar: " + staticVar);
System.out.println("instanceVar: " + instanceVar);
System.out.println("localVarB: " + localVarB);
System.out.println("메서드 B 끝");
}
}
그림: methodB 호출 시 메모리 상태
[Method Area]
+----------------------+
| staticVar = 100 |
+----------------------+
[Heap]
+----------------------+
| obj |
| - instanceVar = 200 |
+----------------------+
[Stack]
+--------------------------+
| [methodB 스택 프레임] |
| - localVarB = 20 |
+--------------------------+
| [methodA 스택 프레임] |
| - paramVar = 5 |
| - localVar = 10 |
+--------------------------+
| [main 스택 프레임] |
| - args |
| - obj |
+--------------------------+
6. methodB 실행
- methodB가 실행되면서 staticVar, instanceVar, localVarB 값을 출력한다. methodB가 끝나면 지역변수인 localVarB가 사라지고, methodB의 스택 프레임도 제거된다. (methodA, main이 남는다.)
그림: methodB 종료 후 메모리 상태
[Method Area]
+----------------------+
| staticVar = 100 |
+----------------------+
[Heap]
+----------------------+
| obj |
| - instanceVar = 200 |
+----------------------+
[Stack]
+--------------------------+
| [methodA 스택 프레임] |
| - paramVar = 5 |
| - localVar = 10 |
+--------------------------+
| [main 스택 프레임] |
| - args |
| - obj |
+--------------------------+
7. methodA 종료
- methodA로 돌아와서 마무리한다. methodA가 끝나면 매개변수 paramVar와 지역변수 localVar가 사라지고, methodA의 스택 프레임도 제거된다. (main만 남는다.)
그림: methodA 종료 후 메모리 상태
[Method Area]
+----------------------+
| staticVar = 100 |
+----------------------+
[Heap]
+----------------------+
| obj |
| - instanceVar = 200 |
+----------------------+
[Stack]
+--------------------------+
| [main 스택 프레임] |
| - args |
| - obj |
+--------------------------+
8. 프로그램 종료
- main 메서드가 끝나면 프로그램이 종료된다. 이때 정적변수인 staticVar는 프로그램 종료 시 사라지며, 인스턴스 변수인 instanceVar도 객체가 메모리에서 해제되면서 사라진다.
그림: 프로그램 종료 후 메모리 상태
[Method Area]
(비어 있음)
[Heap]
(비어 있음)
[Stack]
(비어 있음)
이것으로 알 수 있는 변수들의 생명 주기는 다음과 같다.
변수들의 생명 주기 정리
- staticVar(정적 변수)
- 프로그램 시작 ~ 프로그램 종료
- 프로그램 시작 ~ 프로그램 종료
- instanceVar(인스턴스 변수)
- 객체 생성 시 ~ 객체가 더 이상 사용되지 않을 때
- 객체 생성 시 ~ 객체가 더 이상 사용되지 않을 때
- paramVar, localVar, localVarB(지역 변수, 매게 변수)
- 메서드 호출 시 ~ 메서드 종료 시
4. 스택 프레임의 중요성
스택 프레임의 중요성
- 스택 프레임은 프로그램이 올바르게 작동하기 위해 매우 중요하다. 각 메서드 호출 시 스택 프레임이 생성되고, 메서드가 종료되면 스택 프레임이 제거되기 때문에, 메모리가 효율적으로 관리된다. 또한, 재귀 호출(메서드가 자기 자신을 호출하는 것)에서도 스택 프레임이 중요한 역할을 한다. 스택 프레임 덕분에 각 호출이 독립적으로 처리될 수 있다.
스택 프레임의 예시: 재귀 호출
- 간단한 재귀 호출 예제를 통해 스택 프레임이 어떻게 동작하는지 알아보자.
public class RecursionExample {
public void countDown(int number) {
if (number == 0) {
System.out.println("카운트 완료!");
return;
}
System.out.println("카운트: " + number);
countDown(number - 1); // 자기 자신을 호출
}
public static void main(String[] args) {
RecursionExample example = new RecursionExample();
example.countDown(3);
}
}
실행 결과
카운트: 3
카운트: 2
카운트: 1
카운트 완료!
그림: 재귀 호출 시 스택 프레임 상태
[Stack]
+---------------------------+
| [countDown 스택 프레임 1] |
| - number = 3 |
+---------------------------+
| [main 스택 프레임] |
| - args |
| - example |
+---------------------------+
-> countDown(2) 호출 시
[Stack]
+---------------------------+
| [countDown 스택 프레임 2] |
| - number = 2 |
+---------------------------+
| [countDown 스택 프레임 1] |
| - number = 3 |
+---------------------------+
| [main 스택 프레임] |
| - args |
| - example |
+---------------------------+
-> countDown(1) 호출 시
[Stack]
+---------------------------+
| [countDown 스택 프레임 3] |
| - number = 1 |
+---------------------------+
| [countDown 스택 프레임 2] |
| - number = 2 |
+---------------------------+
| [countDown 스택 프레임 1] |
| - number = 3 |
+---------------------------+
| [main 스택 프레임] |
| - args |
| - example |
+---------------------------+
-> countDown(0) 호출 시
[Stack]
+---------------------------+
| [countDown 스택 프레임 4] |
| - number = 0 |
+---------------------------+
| [countDown 스택 프레임 3] |
| - number = 1 |
+---------------------------+
| [countDown 스택 프레임 2] |
| - number = 2 |
+---------------------------+
| [countDown 스택 프레임 1] |
| - number = 3 |
+---------------------------+
| [main 스택 프레임] |
| - args |
| - example |
+---------------------------+
설명
- countDown(3)이 호출되면 첫 번째 스택 프레임이 생성된다.
- countDown(2)이 호출되면 두 번째 스택 프레임이 쌓인다.
- countDown(1)이 호출되면 세 번째 스택 프레임이 추가된다.
- countDown(0)이 호출되면 네 번째 스택 프레임이 생성되고, 카운트 완료! 를 출력한 후 모든 스택 프레임이 제거된다.
- 이처럼 스택 프레임은 메서드 호출 시마다 쌓이고, 메서드가 끝나면 사라지면서 프로그램이 올바르게 실행될 수 있도록 도와준다.
추가 팁
- 스택 오버플로우(Stack Overflow)
- 너무 많은 스택 프레임이 쌓이면 메모리가 부족해져서 프로그램이 멈추거나 오류가 발생할 수 있다. 특히 재귀 호출을 사용할 때는 종료 조건을 잘 설정해야 한다.
- 너무 많은 스택 프레임이 쌓이면 메모리가 부족해져서 프로그램이 멈추거나 오류가 발생할 수 있다. 특히 재귀 호출을 사용할 때는 종료 조건을 잘 설정해야 한다.
- 가비지 컬렉션(Garbage Collection)
- 힙 영역에 있는 객체는 더 이상 참조되지 않을 때 자바의 가비지 컬렉터가 자동으로 메모리를 정리해 준다. 이를 통해 메모리를 효율적으로 관리할 수 있다.
반응형
'JAVA' 카테고리의 다른 글
[Java] 자바의 약한 복사(Shallow Copy)란? (0) | 2024.09.21 |
---|---|
[Java] 스트림(Stream)에서 가변 변수 사용 시 발생하는 문제 및 해결 방법 (0) | 2024.09.20 |
[Java] 객체지향(OOP)의 특징: 캡슐화 (1) | 2024.09.20 |
[Java] HashTable이 뭘까? (32) | 2024.09.02 |
[Java] Stream의 mutable, immutable 리스트 변환 (toList) (3) | 2024.08.30 |