JAVA

[Java] 스택 프레임과 변수의 생명 주기 이해하기

Stark97 2024. 9. 20. 19:45
 
 
 

자바의 스택 프레임과 변수의 생명 주기를 알아보자.

 

1. 스택 프레임

스택 프레임이란?

  • 컴퓨터가 프로그램을 실행할 때, 메모리를 사용하는 방법 중 하나가 스택(stack)이다. 스택은 책을 쌓아 올리는 것처럼 데이터를 차곡차곡 쌓았다가, 마지막에 넣은 것부터 꺼내는 구조다.

  • 지금부터 알아볼 스택 프레임은 메서드(함수)를 호출할 때마다 생성되는 작은 상자라고 생각하면 된다. 이 상자 안에는 그 메서드에서 사용하는 지역 변수와 매개변수가 들어 있다. 메서드가 끝나면 그 상자는 스택에서 사라지게 된다.

전체 메모리 구조 그림

  • 스택 프레임을 이해하기 위해 먼저 자바의 메모리 구조를 알아보자.
[메모리 구조]
+--------------------------+
|       Method Area        | <-- static 변수
|   (클래스, static 변수)     |
+--------------------------+
|          Heap            | <-- 인스턴스 변수 (객체)
|     (객체들이 저장됨)        |
+--------------------------+
|          Stack           | <-- 스택 프레임
|   [현재 실행 중인 메서드]     |
|  (지역 변수와 매개변수 저장)   |
+--------------------------+

 

메모리 구조 자세히 보기

  1. Method Area (메서드 영역)
    • 역할: 클래스 정보와 static 변수가 저장되는 공간이다.
    • 예시: 클래스의 메서드나 static 변수는 이곳에 저장된다.
    • 특징: 프로그램이 시작될 때 생성되고, 프로그램이 종료될 때까지 유지돼요.

  2. Heap (힙)
    • 역할: 인스턴스 변수가 저장되는 공간이다. 즉, 생성된 객체(인스턴스)들이 이곳에 저장된다.
    • 예시: new 키워드를 사용해 생성한 객체들이 힙에 저장된다.
    • 특징: 객체는 힙에 저장되며, 객체가 더 이상 필요 없을 때 자바의 가비지 컬렉터가 자동으로 메모리를 정리해 준다.

  3. Stack (스택)
    • 역할: 스택 프레임이 저장되는 공간이다. 현재 실행 중인 메서드와 그 메서드의 변수들이 여기에 저장된다.
    • 예시: 메인 메서드나 다른 메서드를 호출할 때마다 새로운 스택 프레임이 생성된다.
    • 특징: 메서드가 호출될 때마다 스택에 쌓이고, 메서드가 종료되면 스택에서 제거된다. 이곳은 아주 빠르게 접근할 수 있는 메모리 영역이다.

스택 프레임의 구성

  • 스택 프레임은 크게 세 부분으로 나눌 수 있다.
  1. 매개변수(Parameter): 메서드가 호출될 때 전달되는 값들이다.
  2. 지역 변수(Local Variables): 메서드 내부에서 선언된 변수들이다.
  3. 반환 주소(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]
(비어 있음)

 

 

 

이것으로 알 수 있는 변수들의 생명 주기는 다음과 같다.



변수들의 생명 주기 정리

  1. staticVar(정적 변수)
    • 프로그램 시작 ~ 프로그램 종료

  2. instanceVar(인스턴스 변수)
    • 객체 생성 시 ~ 객체가 더 이상 사용되지 않을 때

  3. 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)이 호출되면 네 번째 스택 프레임이 생성되고, 카운트 완료! 를 출력한 후 모든 스택 프레임이 제거된다.
  • 이처럼 스택 프레임은 메서드 호출 시마다 쌓이고, 메서드가 끝나면 사라지면서 프로그램이 올바르게 실행될 수 있도록 도와준다.

 

추가 팁

  1. 스택 오버플로우(Stack Overflow)
    • 너무 많은 스택 프레임이 쌓이면 메모리가 부족해져서 프로그램이 멈추거나 오류가 발생할 수 있다. 특히 재귀 호출을 사용할 때는 종료 조건을 잘 설정해야 한다.

  2. 가비지 컬렉션(Garbage Collection)
    • 힙 영역에 있는 객체는 더 이상 참조되지 않을 때 자바의 가비지 컬렉터가 자동으로 메모리를 정리해 준다. 이를 통해 메모리를 효율적으로 관리할 수 있다.

 

 

반응형