본문 바로가기
IT/김영한의실전JAVA-기본_inFlearn

7. 자바 메모리 구조와 static - 자바 메모리 구조 / 스택과 큐 자료 구조 / 스택 영역 / 스택 영역과 힙 영역 / static 변수1~3 / static 메서드1~3 / 문제와 풀이

by for-learn 2025. 2. 15.

7-1. 자바 메모리 구조

자바의 메모리 구조는 크게 메서드 영역, 스택 영역, 영역 3개로 나뉨

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

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

메서드 코드는 메서드 영역에

  • 자바에서 특정 클래스로 100개의 인스턴스를 생성하면,
    힙 메모리에 100개의 인스턴스가 생기고 각각의 인스턴스는 내부에 변수와 메서드를 가진다.
  • 같은 클래스로 부터 생성된 객체라도, 인스턴스 내부의 변수 값은 서로 다를 수 있지만,
    메서드는 공통된 코드를 공유한다.
  • 따라서 객체가 생성될 때, 인스턴스 변수에는 메모리가 할당되지만, 메서드에 대한 새로운 메모리 할당은 없다.
  • 메서드는 메서드 영역에서 공통으로 관리되고 실행된다.
  • 정리하면 인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행한다.

 

7-2. 스택과 큐 자료 구조

스택(Stack)이라는 자료 구조를 알아보자

자료 구조: 데이터를 어떤 식으로 보관하고 관리하는지에 대한 구조

 

스택 구조 => 후입 선출(LIFO, Last In First Out)

프로그램 실행 메서드 호출에는 스택 구조가 적합

 

큐(Queue) => 선입 선출(FIFO, First In First Out)

선착순 프로그램에 적합

 

7-3. 스택 영역

JavaMemoryMain1.java

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

스택구조

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

 

7-4. 스택 영역과 힙 영역

Data.java

package memory;

public class Data {
    private int value;

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

    public int getValue() {
        return value;
    }
}

JavaMemoryMain2.java

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);
        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() 에서 method2() 를 호출할 때 매개변수에 Data 인스턴스의 참조값을 전달

 

  • method1() 이 종료된 직후, method1() 의 스택 프레임이 제거되고 지역 변수 data1 도 함께 제거
  • 이제 x001 참조값을 가진 Data 인스턴스를 참조하는 곳이 더는 없는 프로그램에서 더는 사용하지 않는 객체가 됨.
  • GC(가비지 컬렉션)은 이렇게 참조가 모두 사라진 인스턴스를 찾아서 메모리에서 제거

지역 변수는 스택 영역에, 객체(인스턴스)는 힙 영역에 관리되는 것을 확인

 

7-5. static 변수1

static 키워드는 주로 멤버 변수와 메서드에 사용

멤버 변수에 static 키워드가 왜 필요?

Data1.java (인스턴스 내부에 카운트 저장)

package static1;

public class Data1 {
    public String name;
    public int count;

    public Data1(String name) {
        this.name = name;
        count++;
    }
}
  • 객체가 생성될 때마다 생성자를 통해 인스턴스의 멤버 변수 count 값 증가
  • 예제를 단순하게 만들기 위해 필드에 public 사용

DataCountMain1.java

package static1;

public class DataCountMain1 {
    public static void main(String[] args) {
        Data1 data1 = new Data1("A");
        System.out.println("A count=" + data1.count);

        Data1 data2 = new Data1("B");
        System.out.println("B count=" + data2.count);

        Data1 data3 = new Data1("C");
        System.out.println("C count=" + data3.count);
    }
}

  • 객체를 생성할 때 마다 Data1 인스턴스는 새로 만들어지고 인스턴스에 포함된 count 변수도 새로 만들어짐
  • 인스턴스에 사용되는 멤버 변수 count 값은 인스턴스끼리 서로 공유되지 않음
  • 문제를 해결하려면 변수를 서로 공유해야 함

Counter.java (외부 인스턴스에 카운트 저장)

package static1;

public class Counter {
    public int count;
}

Data2.java 

package static1;

public class Data2 {
    public String name;

    public Data2(String name, Counter counter) {
        this.name = name;
        counter.count++;
    }
}

DataCountMain2.java

package static1;

public class DataCountMain2 {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Data2 data1 = new Data2("A", counter);
        System.out.println("A count=" + counter.count);

        Data2 data2 = new Data2("A", counter);
        System.out.println("B count=" + counter.count);

        Data2 data3 = new Data2("A", counter);
        System.out.println("C count=" + counter.count);
    }
}

  • Data2 클래스와 관련된 일인데, Counter라는 별도의 클래스 추가 사용
  • 생성자의 매개변수도 추가되고, 생성자가 복잡해짐.

 

7-6. static 변수2

특정 클래스에서 공용으로 사용할 수 있는 변수 만들기

Data3.java (static 변수 사용)

package static1;

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

    public Data3(String name) {
        this.name = name;
        count++;
    }
}
  • 멤버 변수 앞에 static을 붙이면, static 변수 = 정적 변수 = 클래스 변수 라 함

DataCountMain3.java

package static1;

public class DataCountMain3 {
    public static void main(String[] args) {
        Data3 data1 = new Data3("A");
        System.out.println("A count=" + Data3.count);

        Data3 data2 = new Data3("B");
        System.out.println("B count=" + Data3.count);
        
        Data3 data3 = new Data3("C");
        System.out.println("C count=" + Data3.count);
    }
}

  • count 정적 변수에 접근하는 방법.
  • Data3.count와 같이 클래스명에 .dot을 사용해 클래스에 직접 접근하는 것 같음.

  • static이 붙은 멤버 변수는 메서드 영역에서 관리

※ static 변수는 예로, 클래스인 붕어빵 틀이 특별히 관리하는 변수

(붕어빵 틀 = 1, 클래스 변수 = 1), (붕어빵 = n, 인스턴스 변수 = n)

 

7-7. static 변수3

멤버 변수(필드)의 종류 (용어 정리)

  • 인스턴스 변수
    • static이 붙지 않은 멤버 변수
    • 인스턴스를 생성해야 사용 가능, 인스턴스에 소속
    • 인스턴스를 만들 때마다 새로 생성
  • 클래스 변수 (= 정적 변수, static 변수)
    • static이 붙은 멤버 변수
    • 인스턴스와 무관하게 클래스를 통해 바로 접근해서 사용 가능, 클래스 자체에 소속
    • 자바 프로그램을 시작할 때 딱 1개가 생성, 여러 곳에서 공유할 목적

변수와 생명 주기

  • 지역 변수(매개 변수 포함) : 생존 주기 짦음
    • 스택 영역에 있는 스택 프레임 안에 보관
    • 메서드가 종료되면 스택 프레임이 제거되고 이때 포함된 지역 변수도 제거
  • 인스턴스 변수 : 지역 변수보다 생존 주기 김.
    • 힙 영역을 사용, GC(가비지 컬렌션)가 발생하기 전까지 생존
    • 인스턴스 변수는 동적으로 생성되고 제거된다.
  • 클래스 변수 : 가장 긴 생존 주기
    • 메서드 영역의 static 영역에 보관, 메서드 영역은 프로그램 전체에서 사용하는 공용 공간
    • 해당 클래스가 JVM에 로딩되는 순간 클래스 변수 생성, JVM 종료될 때까지 생존
    • 정적인 이유는 프로그램 실행 시점에 만들어지고, 프로그램 종료 시점에 제거되기 때문

DataCountMain3.java (추가 - 정적 변수 접근법)

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

// 클래스를 통한 접근
System.out.println(Data3.count);
  • 둘다 정적 변수에 접근
  • 정적 변수의 경우 인스턴스를 통한 접근은 추천하지 않는다. 
    코드를 읽을 때 마치 인스턴스 변수에 접근하는 것 처럼 오해할 수 있기 때문
  • 정적 변수는 클래스에서 공용으로 관리하기에 클래스를 통해서 접근

 

7-8. static 메서드1

DecoUtil1.java

package static2;

public class DecoUtil1 {
    public String deco(String str) { // 문자열을 꾸미는 기능 제공
        String result = "*" + str + "*";
        return result;
    }
}

DecoMain1.java

package static2;

public class DecoMain1 {
    public static void main(String[] args) {
        String s = "hello java";
        DecoUtil1 utils = new DecoUtil1();
        String deco = utils.deco(s);

        System.out.println("before: " + s);
        System.out.println("after: " + deco);
    }
}

  • deco() 메서드를 호출하기 위해서는 DecoUtil1 의 인스턴스를 먼저 생성해야 한다.
  • deco() 라는 기능은 멤버 변수도 없고, 단순히 기능만 제공할 뿐
  • 인스턴스가 필요한 이유는 멤버 변수(인스턴스 변수)등을 사용하는 목적이 큰데,
    이 메서드는 사용하는 인스턴스 변수도 없고 단순히 기능만 제공

DecoUtil2.java (static 메서드)

package static2;

public class DecoUtil2 {
    public static String deco(String str) { // static 추가
        String result = "*" + str + "*";
        return result;
    }
}

DecoMain2.java

package static2;

public class DecoMain2 {
    public static void main(String[] args) {
        String s = "hello java";
        String deco = DecoUtil2.deco(s);

        System.out.println("before: " + s);
        System.out.println("after: " + deco);
    }
}
  • static 이 붙은 정적 메서드는 정적 변수처럼 인스턴스 생성 없이 클래스 명을 통해서 바로 호출
  • 정적 메서드 덕분에 불필요한 객체 생성 없이 편리하게 메서드를 사용
클래스 메서드(= 정적 메서드): 인스턴스 생성 없이 클래스를 통해 호출 가능
인스턴스 메서드: 인스턴스를 생성해야 호출 가능

 

7-9. static 메서드2

정적 메서드 사용법

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

DecoData.java

package static2;

public class DecoData {

    private int instanceValue;
    private static int staticValue;

    public static void staticCall() {
        //instanceValue++; // 인스턴스 변수 접근, compile error
        //instanceMethod(); // 인스턴스 메서드 접근, compile error
        staticValue++; // 정적 변수 접근
        staticMethod(); // 정적 메서드 접근
    }
    
    public void instanceCall() {
        instanceValue++; // 인스턴스 변수 접근
        instanceMethod(); // 인스턴스 메서드 접근
        staticValue++; // 정적 변수 접근
        staticMethod(); // 정적 메서드 접근
    }
    
    private void instanceMethod() {
        System.out.println("instanceValue=" + instanceValue);
    }
    private static void staticMethod() {
        System.out.println("staticValue=" + staticValue);
    }
}
  • static 메서드는 static 변수, static 메서드 접근 가능 => 인스턴스 (변수, 메서드) 접근 불가

DecoDataMain.java

package static2;

public class DecoDataMain {
    public static void main(String[] args) {
        System.out.println("1.정적 호출");
        DecoData.staticCall();

        System.out.println("2.인스턴스 호출1");
        DecoData data1 = new DecoData();
        data1.instanceCall();

        System.out.println("3.인스턴스 호출2");
        DecoData data2 = new DecoData();
        data2.instanceCall();
    }
}

 

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

  • 정적 메서드는 클래스 이름을 통해 바로 호출 가능 => 참조값 없이 호출
  • 특정 인스턴스의 내부 기능을 사용하려면 참조값을 알아야 함
  • 정적 메서드 내부에서 참조값이 없어 인스턴스 변수나 인스턴스 메서드를 사용할 수 없다.

(외부에서) 객체의 참조값을 직접 매개변수로 전달

=> 정적 메서드도 인스턴스의 변수나 메서드를 호출 가능

public static void staticCall(DecoData data) {
    data.instanceValue++;
    data.instanceMethod();
}

 

 

7-10. static 메서드3

멤버 메서드의 종류 (용어 정리)

  • 인스턴스 메서드static이 붙지 않은 멤버 메서드
  • 클래스 메서드 (= 정적 메서드, static 메서드)static이 붙은 멤버 메서드 

정적 메서드 활용

  • 객체 생성이 필요없이 메서드의 호출만으로 필요한 기능을 수행할 때 사용
  • 간단한 메서드 하나로 끝나는 유틸리티성 메서드에 자주 사용
  • 수학의 여러가지 기능을 담은 클래스를 만들 때, 인스턴스 변수 없이 입력한 값을 계산하고 반환하는 것이 대부분
    이럴 때 정적 메서드를 사용해서 유틸리티성 메서드를 만들면 좋음

DecoDataMain.java (추가 - 정적 메서드 접근법)

// 추가
// 인스턴스를 통한 접근
DecoData data3 = new DecoData();
data3.staticCall();

// 클래스를 통한 접근
DecoData.staticCall();

  • 둘은 차이 없이 결과적으로 정적 메서드에 접근.
  • But, 인스턴스를 통한 접근은 추천하지 않음(인스턴스 메서드에 접근하는 것 처럼 보일 수 있기 때문)

static import

alt+enter => Add on-demand static import for 'static2.DecoData'

//import static static2.DecoData.staticCall;
import static static2.DecoData.*; // *: 전체 static

// 클래스명 생략 가능
DecoData.staticCall(); => staticCall()
DecoData.staticCall(); => staticCall()
DecoData.staticCall(); => staticCall()

 

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

  • 인스턴스 생성 없이 실행하는 가장 대표적인 메서드가 바로 main() 메서드
  • main() 메서드는 프로그램을 시작하는 시작점이 되는데, main() 메서드가 static 이기 때문
  • 정적 메서드인 main() 이 호출하는 메서드에는 정적 메서드를 사용
  • 정확히 말하자면 정적 메서드는 같은 클래스 내부에서 정적 메서드만 호출 가능
// 예시
package oop1;
public class ValueDataMain {
    public static void main(String[] args) {
        ValueData valueData = new ValueData();
        add(valueData);
        add(valueData);
        add(valueData);
        System.out.println("최종 숫자=" + valueData.value);
    }
    static void add(ValueData valueData) {
        valueData.value++;
        System.out.println("숫자 증가 value=" + valueData.value);
    }
}

 

7-11. 문제와 풀이

Car.java

package static2.ex;

public class Car {

    private static int cars;
    private String name;

    void Car(String name) {
        System.out.println("차량 구입, 이름: "+name);
        this.name = name;
        cars++;
    }

    static void showTotalCars() {
        System.out.println("구매한 차량 수: " + cars);
    }
}

 

MathArrayUtils.java

package static2.ex;

public class MathArrayUtils {

    private MathArrayUtils() {
        //private 인스턴스 생성을 막는다.
    }

    public static int sum(int[] array) {
        int sum = 0;
        for (int i : array) {
            sum+=i;
        }
        return sum;
    }

    public static double average(int[] array) {
        return sum(array)/(double)array.length;
    }

    public static int min(int[] array) {
        int minValue = array[0];
        for (int i : array) {
            if (i < minValue) {
                minValue = i;
            }
        }
        return minValue;
    }

    public static int max(int[] array) {
        int maxValue = array[0];
        for (int i : array) {
            if (i > maxValue) {
                maxValue = i;
            }
        }
        return maxValue;
    }
}

 

인스턴스 생성 막기

private MathArrayUtils() {}