Java

[Java] 자료 구조 스택

쌈뽕코딩 2024. 3. 11. 19:50

자바의 메모리 구조

 

 

자바의 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역 3개로 나눌 수 있습니다.

  • 메서드 영역: 클래스 정보를 보관한다. 이 클래스 정보가 붕어빵 틀이다.
  • 스택 영역: 실제 프로그램이 실행되는 영역이다. 메서드를 실행할 때 마다 하나씩 쌓인다.
  • 힙 영역: 객체(인스턴스)가 생성되는 영역이다. new 명령어를 사용하면 이 영역을 사용한다. 쉽게 이야기해서 붕어빵 틀로부터 생성된 붕어빵이 존재하는 공간이다. 배열도 이 영역에 생성된다.

 

 

메서드 영역(Method Area): 메서드 영역은 프로그램을 실행하는데 필요한 공통 데이터를 관리한다.

이 영역은 모든 영역에서 공유한다.

  • 클래스 정보: 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재한다.
  • static 영역: static 변수들을 보관한다.
  • 런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관한다. 예를 들어 프로그램에 "hello" 라는 리터럴문자가 있으면 이런 문자를 공통으로 묶어서 관리한다. 이 외에도 프로그램을 효율적으로 관리하기 위한 상수들을 관리한다.

스택 영역(Stack Area): 자바 실행 시, 하나의 실행 스택이 생성된다.

각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함한다.

  • 스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이다. 메서드를 호출할 때 마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거된다.

힙 영역(Heap Area): 객체(인스턴스)와 배열이 생성되는 영역이다. 가비지 컬렉션(GC)이 이루어지는 주요 영역이며, 더 이상 참조하지 않는 객체는 GC에 의해 제거된다.

 

참고: 스택 영역은 각 쓰레드별로 하나의 실행 스택이 생성된다.
따라서 쓰레드 수 만큼 스택 영역이 생성된다. 지금은 쓰레드를 1개만 사용하므로 스택 영역도 하나이다.

 

자바에서 특정 클래스로 100개의 인스턴스를 생성한다고 하면, 힙 메모리에 100개의 인스턴스가 생깁니다.

각각의 인스턴스는 내부에 변수와 메서드를 가집니다. 같은 클래스로 부터 생성된 객체라도,

인스턴스 내부의 변수(필드) 값은 서로 다를수 있지만, 메서드는 공통된 코드를 공유합니다.

따라서 객체가 생성될 때, 인스턴스 변수(필드)에는 메모리가 할당 되지만, 메서드에 대한 새로운 메모리 할당은 없습니다.

메서드는 메서드 영역에서 공통으로 관리되고 실행되고 있습니다. 

 

인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행하고 있습니다.

 

스택과 큐 자료 구조

자바 메모리 구조 중 스택 영역에 대해 알아보기 전에 먼저 스택(Stack)이라는 자료 구조에 대해서 알아봅시다.

 

스택 구조

다음과 같은 1, 2, 3 이름표가 붙은 블럭이 있다고 가정해봅시다.

이 블럭을 다음과 같이 생긴 통에 넣는다고 생각해봅시다.

위쪽만 열려있기 때문에 위쪽으로 블럭을 넣고, 위쪽으로 블럭을 빼내야 할 것 입니다. 

 

블럭은 1 => 2 => 3 순서대로 넣을 수 있습니다.

이번에는 반대로 블럭을 빼보겠습니다.

블럭을 빼야한다면 위에서 부터 순서대로 빼야합니다.

3 => 2=> 1 순서로 뺼 수 있습니다.

 

 

후입 선출(LIFO, Last in First On):
여기서 가장 마지막에 넣은 3번이 가장 먼저 나옵니다.

이렇게 나중에 넣은 것이 가장 먼저 나오는 것을 후입 선출이라 하고, 이런 자료 구조를 스택이라 한다.

 

선입 선출(FIFO, First in First On):

후입 선출과 반대로 가장 먼저 넣은 것이 가장 먼저 나오는 것을 선입 선출이라고 한다.

이런 자료 구조를 큐(Queue)라고 한다.

 

큐(Queue) 구조

 

 

1(넣기) => 2 (넣기) => 3 (넣기) => 1 (빼기) => 2 (빼기) => 3 (빼기)

 

이런 자료 구조는 각자 필요한 영역이 있습니다. 예를 들어 선착순 이벤트를 하는데 고객이 대기해야 한다면

큐 자료 구조를 사용해야 됩니다. 반대로 프로그램 실행과 메서드 호출에는 스택 구조가 적합합니다.

스택 구조가 무엇인지 알았으니, 자바에서 스택영역이 어떤 방식으로 작동하는지 알아봅시다.

 

 

스택 영역

다음 코드를 실행하게 되면 스택 영역에 어떤 변화가 생기는지 알아봅시다.

public class JavaMemoryMain1 {

    public static void main(String[] args) {

        System.out.println("main start");
        method1(10);
        System.out.println("main end");
    }
    static void method1(int m1) {
        System.out.println("method1 start");
        int cal = m1 * 2;
        method2(cal);
        System.out.println("method1 end");
    }
    static void method2(int m2) {
        System.out.println("method2 start");
        System.out.println("method2 end");
    }
}

 

실행 결과

main start
method1 start
method2 start
method2 end
method1 end
main end

 

호출 그림

  • 처음 자바 프로그램을 실행하면 main( ) 을 실행한다. 이 때 main( )을 위한 스택 프레임이 하나 생성된다.
  • main( ) 은 method1( ) 을 호출한다. method1( ) 스택 프레임이 생성된다.
  • method1( ) 은 m1, cal 지역 변수(매개변수 포함)를 가지므로 해당 지역 변수들이 스택 프레임에 포함된다.
  • method1( ) 은 method2( ) 를 호출한다. method2( ) 스택 프레임이 생성된다. method2( )는 m2 지역변수(매개변수)를 가지므로 해당 지역 변수가 스택 프레임에 포함된다.

 

종료 그림

  • method2( ) 가 종료된다. 이 때 method2( ) 스택 프레임이 제거되고, 매개변수 m2도 제거된다. method2( ) 스택 프레임이 제거 되었으므로 프로그램은 method1( ) 로 돌아간다. 물론 method1( )을 처음부터 시작하는 것이 아닌 method1( ) 에서 method2( ) 를 호출한 지점으로 돌아간다.
  • method1( ) 이 종료된다. 이때 method1( ) 스택 프레임이 제거되고, 지역 변수(매개변수 포함) m1, cal 도 제거된다. 프로그램은 main( )으로 돌아간다.
  • main( ) 이 종료된다. 더 이상 호출할 메서드가 없고, 스택 프레임도 완전히 비워졌다. 자바는 프로그램을 정리하고 종료한다.

정리

  • 자바는 스택 영역을 사용해서 메서드 호출과 지역 변수(매개변수 포함)를 관리한다.
  • 메서드를 계속 호출하면 스택 프레임이 계속 쌓인다.
  • 지역 변수(매개변수 포함)는 스택 영역에서 관리한다.
  • 스택 프레임이 종료되면 지역 변수도 함께 제거된다.
  • 스택 프레임이 모두 제거되면 프로그램도 종료된다.

 

스택 영역과 힙 영역

 

이번에는 스택 영역과 힙 영역이 함께 사용되는 경우를 알아봅시다.

public class Data {

    private int value;

    public Data(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }
public class JavaMemoryMain2 {

    public static void main(String[] args) {

        System.out.println("main start");
        method1();
        System.out.println("main end");
    }
    static void method1() {
        System.out.println("method1 start");
        Data data1 = new Data(10);
        method2(data1);
        System.out.println("method1 end");
    }
    static void method2(Data data2) {
        System.out.println("method2 start");
        System.out.println("data.value = " + data2.getValue());
        System.out.println("method2 end");
    }
}
  • main( ) => method1 => method2 순서로 호출하는 단순한 코드이다.
  • method1( ) 에서 Data 클래스의 인스턴스를 생성한다.
  • method1( ) 에서 method2( ) 를 호출할 때 매개변수에 Data 인스턴스의 참조값을 전달한다.

실행 결과

main start
method1 start
method2 start
data.value = 10
method2 end
method1 end
main end

 

main( ) 실행

  • 처음에 main( ) 메서드를 실행한다. main( ) 스택 프레임이 생성된다.

method1( ) 실행

  • main( ) 에서 method1( )을 실행한다. method1( ) 스택 프레임이 생성된다.
  • method1( )은 지역 변수로 Data data1 을 가지고 있다. 이 지역 변수도 스택 프레임에 포함된다.
  • method1( )은 new Data(10)을 사용해서 힙 영역에 Data 인스턴스를 생성한다. 그리고 참조값을 data1에 보관한다.

method2( ) 실행

  • method1( ) 은 method2( ) 를 호출하면서 Data data2 매개변수에 x001 참조값을 넘긴다.
  • 이제 method1( ) 에 있는 data1과 method2( )에 있는 data2 지역 변수(매개변수 포함)는 둘다 같은 x001 인스턴스를 참조한다.

method2( ) 종료

 

  • method2( ) 가 종료된다. method2( ) 스택  프레임이 제거되면서 매개변수 data2도 함께 제거된다.

method1( ) 종료

  • method1( ) 이 종료된다. method1( ) 의 스택 프레임이 제거되면서 지역 변수 data1도 함께 제거된다.

method1( ) 종료 직후

  • method1( ) 이 종료된 직후의 상태를 보자. method1( ) 의 스택 프레임이 제거되고 지역 변수 data1 도 함께 제거되었다.
  • 이제 x001 참조값을 가진 Data 인스턴스를 참조하는 곳이 더는 없다.
  • 참조하는 곳이 없으므로 사용되는 곳도 없다. 결과적으로 프로그램은 더는 사용하지 않는 객체인 것이다. 이런 객체는 메모리만 차지하게 된다.
  • GC(가비지 컬렉션)은 이렇게 참조가 모두 사라진 인스턴스를 찾아서 메모리에서 제거한다.

참고: 힙 영역 외부가 아닌 힙 영역 안에서만 인스턴스 끼리 서로 참조하는 경우에도 GC의 대상이 된다.

 

정리

지역 변수는 스택 영역에, 객체(인스턴스)는 힙 영역에 관리되는 것을 확인했습니다. 이제 나머지 하나가 남았습니다.

바로 메서드 영역입니다. 메서드 영역이 관리하는 변수도 있습니다. 이것을 이해하기 위해서는 먼저 static 키워드를 알아야 합니다.static 키워드는 메서드 영역과 밀접한 관계가 있습니다.

 

 

public class Data3 {

    public String name;
    public static int count;
}

 

예제 코드에서 name, count 둘다 멤버 변수입니다.

멤버 변수(필드)는 static이 붙은 것과 아닌 것에 따라 다음과 같이 분류할 수 있습니다.

 

멤버 변수(필드)의 종류

인스턴스 변수: static이 붙지 않은 멤버 변수

  • static이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있다. 따라서 인스턴스 변수라 한다.
  • 인스턴스 변수는 인스턴스를 만들 때 마다 새로 만들어진다.

클래스 변수: static이 붙은 멤버 변수

  • 클래스 변수, 정적 변수, static 변수 등으로 부른다.
  • static이 븥은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있다. 따라서 클래스 변수라 한다.
  • 클래스 변수는 자바 프로그램을 시작할 때 딱 1개가 만들어진다. 인스턴스와는 다르게 보통 여러곳에서 공유하는 목적으로 사용된다.

 

변수와 생명 주기

  • 지역 변수(매개변수 포함): 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관된다. 메서드가 종료되면 스택 프레임도 제거 되는데 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거된다. 따라서 지역 변수는 생존 주기가 짧다.
  • 인스턴스 변수: 인스턴스에 있는 멤버 변수를 인스턴스 변수라고 한다. 인스턴스 변수는 힙 영역을 사용한다. 힙 영역은 GC가 발생하기 전까지는 생존하기 때문에 보통 지역 변수보다 생존 주기가 길다.
  • 클래스 변수: 클래스 변수는 메서드 영역의 static 영역에 보관되는 변수이다. 메서드 영역은 프로그램 전체에서 사용하는 공용 공간이다. 클래스 변수는 해당 클래스가 JVM에 로딩 되는 순간 생성된다. 그리고 JVM이 종료될 때 까지 생명주기가 이어진다. 따라서 가장 긴 생명주기를 가진다.

static이 정적이라는 이유는 바로 여기에 있습니다. 힙 영역에 생성되는 인스턴스 변수를 동적으로 생성되고, 제거됩니다.

반면에 static인 정적 변수는 거의 프로그램 실행 시점에 딱 만들어지고, 프로그램 종료 시점에 제거됩니다.

정적 변수는 이름 그대로 정적입니다.

 

 

정적 변수 접근법

static 변수는 클래스를 통해 바로 접근할 수도 있고, 인스턴스를 통해서도 접근할 수 있습니다.

// 추가
// 인스턴스를 통한 접근
Data3 data4 = new Data3("D");
System.out.println(data4.count);
// 클래스를 통한 접근
System.out.println(Data3.count);

 

둘다 차이는 없다. 둘다 결과적으로 정적 변수에 접근합니다.

 

인스턴스를 통한 접근 data4.count

정적 변수의 경우 인스턴스를 통한 접근은 추천하지 않는다.

왜냐하면 코드를 읽을 때 마치 인스턴스 변수에 접근하는 것처럼 오해할 수 있기 때문이다.

 

클래스를 통한 접근 Data3.count

정적 변수는 클래스에서 공용으로 관리하기 때문에 클래스를 통해서 접근하는 것이 더 명확하다.

따라서 정적 변수에 접근할 때는 클래스를 통해서 접근하자.