자바 메모리 구조

비유

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

 

실제

  • 메서드 영역(Method Area): 프로그램을 실행하는 데 필요한 공통 데이터 관리. 프로그램의 모든 영역에서 공유
    • 클래스 정보: 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재
    • static 영역: static 변수들을 보관함
    • 런타임 상수 풀: 프로그램을 실행하는 데 필요한 공통 리터럴 상수를 보관한다. 예를 들어서 프로그램에 "hello" 라는 리터럴 문자가 있으면 이런 문자를 공통으로 묶어서 관리한다. 이 외에도 프로그램을 효율적으로 관리하기 위한 상수들을 관리함
  • 스택 영역(Stack Area): 자바를 실행하면 하나의 실행 스택이 생성된다. 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함한다.
    • 스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이다. 메서드를 호출할 때마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거된다.
  • 힙 영역(Heap Area): 객체(인스턴스)와 배열이 생성되는 영역. 가비지 컬렉션(GC)이 이루어지는 주요 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거된다.

 

 


스택과 큐 자료 구조

 

1) 스택(Stack) 자료 구조: 후입 선출(LIFO, Last In First Out)

블럭을 1 → 2 → 3 순서대로 넣는다.

 

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

블럭은 3 → 2 → 1 순서로 뺄 수 있다.

 

가장 마지막에 넣은 3번이 가장 먼저 나온다.

=> 나중에 넣은 것이 가장 먼저 나오는 것: 후입 선출 = 스택

 

 

2) 큐(Queue) 자료 구조: 선입 선출(FIFO, First In First Out)

블럭을 1 → 2 → 3 순서대로 넣는다.

뺄 때도 1 → 2 → 3 순서대로 뺀다.

선착순 이벤트를 하는데 고객이 대기해야 한다면 큐 자료 구조를 사용해야 한다.

 

한편, 프로그램 실행과 메서드 호출에는 스택 구조가 적합하다.

 


스택 영역

package memory;

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");
    }
}

실행 결과

 

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

 


스택 영역과 힙 영역

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

package memory;

public class Data {
    private int value;

    public Data(int value){
        this.value = value;
    }

    public int getValue(){
        return value;
    }
}
package memory;

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); //Data 클래스의 인스턴스 생성
        method2(data1); //method2 매개변수에 Data 인스턴스의 참조값을 전달
        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() 메서드를 실행한다. main() 스택 프레임이 생성된다.

 

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

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

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

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

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

 

정리

  • 스택 영역: 지역 변수
  • 힙 영역: 객체(인스턴스)
  • 메서드 영역: static 변수

 


static 변수

특정 클래스에서 공용으로 함께 사용할 수 있는 변수

static 변수 = 정적 변수 = 클래스 변수

static 변수는 쉽게 이야기해서 클래스인 붕어빵 틀이 특별히 관리하는 변수이다. 붕어빵 틀은 1개이므로 클래스 변수도 하나만 존재한다. 반면에 인스턴스 변수는 붕어빵인 인스턴스의 수 만큼 존재한다.

public class Data3 {
     public String name;
     public static int count; //static
}

멤버 변수(필드)의 종류

  1. 인스턴스 변수(non-static)
    - static이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있다.
    - 인스턴스 변수는 인스턴스를 만들 때마다 새로 만들어진다.
    - non-static은 객체를 생성한 뒤 '객체명.' 식으로 접근해야 한다.
  2. 클래스 변수(static) = 정적 변수 = static 변수
    - static이 붙은 멤버 변수는 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있다.
    - 보통 여러 곳에서 공유하는 목적으로 사용된다.
    - static은 '클래스명.' 식으로 접근해야 한다.

 

변수와 생명주기

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

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

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

 

정적 변수 접근법

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

  1. 클래스명.변수 식으로 접근
    ex) Data1.count
  2. 객체명.변수 식으로 접근
    ex) data1.count
    정적 변수의 경우 인스턴스를 통한 접근은 추천하지 않는다. 왜냐하면 코드를 읽을 때 마치 인스턴스 변수에 접근하는 것처럼 오해할 수 있기 때문이다.

 


Static 메서드

멤버 메서드의 종류

  1. 인스턴스 메서드(non-static)
    - static이 붙지 않은 멤버 메서드는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있다.
    - 인스턴스 메서드는 인스턴스를 만들 때마다 새로 만들어진다.
    - non-static은 객체를 생성한 뒤 '객체명.' 식으로 접근해야 한다.
  2. 클래스 메서드(static) = 정적 메서드 = static 메서드
    - static이 붙은 멤버 메서드는 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있다.
    - 보통 여러 곳에서 공유하는 목적으로 사용된다.
    - static은 '클래스명.' 식으로 접근해야 한다.
    - static 메서드는 static 변수만 사용할 수 있다.

 

정적 메서드 사용법

  • static 메서드는 static만 사용할 수 있다.
    • 클래스 내부의 기능을 사용할 때, 정적 메서드는 static이 붙은 정적 메서드나 정적 변수만 사용할 수 있다.
    • 클래스 내부의 기능을 사용할 때, 정적 메서드는 인스턴스 변수나, 인스턴스 메서드를 사용할 수 없다.
  • 반대로 모든 곳에서 static을 호출할 수 있다.
    • 정적 메서드는 공용 기능이다. 따라서 접근 제어자만 허락한다면 클래스를 통해 모든 곳에서 static 을 호출할 수 있다.

 

정적 메서드가 인스턴스의 기능을 사용할 수 없는 이유

정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있다. 그래서 인스턴스처럼 참조값의 개념이 없다. 특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데, 정적 메서드는 참조값 없이 호출한다. 따라서 정적 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없다.

 

정적 메서드 접근법

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

  1. 클래스명.메서드 식으로 접근
    ex) Data.staticCall();
  2. 객체명.메서드 식으로 접근
    ex) Data data = new Data();
    data.staticCall();
    정적 메서드의 경우 인스턴스를 통한 접근은 추천하지 않는다. 왜냐하면 코드를 읽을 때 마치 인스턴스 변수에 접근하는 것처럼 오해할 수 있기 때문이다.

static import

정적 메서드를 사용할 때 해당 메서드를 다음과 같이 자주 호출해야 한다면 static import 기능을 고려하자.

DecoData.staticCall();
DecoData.staticCall();
DecoData.staticCall();

 

import static 기능을 사용하면 클래스 명을 생략하고 메서드를 호출할 수 있다.

import static static2.DecoData.staticCall;
import static static2.DecoData.*;
staticCall();
staticCall();
staticCall();

 

  1. import static static2.DecoData.staticCall;
    특정 클래스의 정적 메서드 하나만 적용하려면 생략할 메서드 명을 적어주면 된다.
  2. import static static2.DecoData.*;
    특정 클래스의 모든 정적 메서드에 적용하려면  * 을 사용하면 된다.

 

main() 메서드는 정적 메서드

인스턴스 생성 없이 실행하는 가장 대표적인 메서드가 바로 main() 메서드이다.

main() 메서드는 프로그램을 시작하는 시작점이 되는데, 생각해보면 객체를 생성하지 않아도 main() 메서드가 작동했다. 이것은 main() 메서드가 static이기 때문이다.

 

정적 메서드는 정적 메서드만 호출할 수 있다. 따라서 정적 메서드인 main()이 호출하는 메서드에는 정적 메서드를 사용했다.

물론 더 정확히 말하자면 정적 메서드는 같은 클래스 내부에서 정적 메서드만 호출할 수 있다. 따라서 정적 메서드인 main() 메서드가 같은 클래스에서 호출하는 메서드도 정적 메서드로 선언해서 사용했다.

 

main() 메서드와 static 메서드 호출 예

public class ValueDataMain {
     public static void main(String[] args) {
     ValueData valueData = new ValueData();
     add(valueData);
     }
     static void add(ValueData valueData) {
     valueData.value++;
     System.out.println("숫자 증가 value=" + valueData.value);
    }
}

+ Recent posts