🖥️ Back/Java

김영한 실전 자바 기본편 - 자바 메모리 구조와 static

지구용사 2025. 7. 15. 16:08

자바 메모리 구조

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

 

메서드 영역: 클래스 정보를 보관한다. 쉽게 생각하면 클래스 정보가 붕어빵 틀이다.

스택 영역: 실제 프로그램이 실행되는 영역으로 메서드가 실행할 때마다 하나씩 쌓인다.

힙 영역: 객체(인스턴스)가 생성되는 영역으로 new 명령어를 사용하면 이 영역을 사용한다. 틀로 만든 붕어빵들이 존재함

 

위 내용은 쉽게 비유한 것으로 실제로는 다음과 같다.

 

✔️ 메서드 영역(Method Area)

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

그래서 이 영역은 프로그램의 모든 영역에서 공유하고 있다.

  - 클래스 정보: 클래스의 실행코드(바이트 코드), 필드, 메서드와 생성자 코드 등 모든 실행코드

  - static 변수들도 보관한다. 이건 뒤에서 더 다룸

  - 런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관한다.

 

✔️ 스택 영역(Stack Area)

자바 실행할 때 하나의 실행 스텍이 생성된다.

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

메서드 호출할 때마다 하나의 스택 프레임이 쌓이고, 메서드 종료되면 해당 스택 프레임은 제거된다.

 

✔️ 힙 영역(Heap Area)

객체(인스턴스)와 배열이 생성되는 영역이다.

가비지 컬렉션(GC)이 이루어지는 주요 영역으로 더이상 참조하지 않는 객체는 GC에 의해 제거된다.

 

자바에서 특정 클래스로 100개의 인스턴스를 생성하면 힙 메모르에 100개의 인스턴스가 생긴다.

각각의 인스턴스는 내부에 변수와 메서드를 가지는데 이중에 공통되는 코드가 있을 것이다.

객체가 생성될 때 인스턴스의 변수는 메모리가 할당되지만 메서드에 대한 새로운 메모리 할당은 없고 메서드 영역에서 관리한다.

 

 

스택과 큐 자료 구조

자바 메모리 구조 중 스택 영역을 알아보기 전에 스택이라는 자료구조를 알아보자

 

후입선출(LIFO, Last In First Out)

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

이러한 자료 구조를 스택이라 한다.

 

선입 선출(FIFO, First In First Out)

후입 선출과 다르게 가장 먼저 넣은 것이 먼저 나온다.

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

 

 

스택 영역

자바는 스택영역을 사용해서 메서드 호출과 지역변수(매개변수 포함)을 관리한다.

스택 프레임이 종료되면 지역변수도 함께 제거되고, 스택 프레임이 모두 제거되면 프로그램은 종료된다.

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

 

 

static 변수

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

 

생성된 객체의 수를 세어야 한다.

객체가 생성될 때마다 생성자를 통해 인스턴스의 멤버 변수인 count 값을 증가시키록 해본다.

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

    public Data1(String name) {
        this.name = name;
        count++;
    }
}
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 변수도 새로 만들어지기 때문이다.

//결과
A count=1
B count=1
C count=1

 

제대로 카운트하기 위해서는 인스턴스에 사용되는 멤버변수 count가 인스턴스끼리 공유되지 않는다.

그래서 카운트 값을 저장하는 별도의 객체를 만들어본다.

public class Counter {
    public int count;
}

 

Counter를 사용하기 위해서 생성자에서 Counter 인스턴스를 전달받아야한다.

public class Data2 {
    public String name;

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

 

Data2 관련 일인데 Counter라는 별도의 클래스를 추가해야한다.

그리고 생성자의 매개변수도 추가되고 복잡하다..!

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("B", counter);
        System.out.println("B count=" + counter.count);

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

 

특정 클래스에서 공용으로 함께 사용되는 변수를 만들어 관리하면 편할 것이다.

이것을 static 키워드를 사용하면 만들 수 있다.

 

멤버 변수에 static을 붙이면 static 변수, 정적 변수 또는 클래스 변수라고 한다.

객체가 생성되면 생성자에서 정적 변수 count의 값을 증가시킨다.

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

    public Data3(String name) {
        this.name = name;
        count++;
    }
}
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);
    }
}

 

static이 붙은 멤버변수는 인스턴스 영역에 생성되지 않고 메서드 영역에서 관리한다.

 

용어가 헷갈리기 때문에 용어를 정리한다.

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

 

✔️ 멤버변수(필드)의 종류

인스턴스 변수: static이 붙지 않는 멤버변수로 인스턴스 생성될 때마다 새로 만들어진다. 예) name

클래스 변수: static이 붙은 멤버변수로 인스턴스와 무관하게 클래스에 바로 접근할 수 있다.

자바 프로그램을 시작할 때 딱 1개만 만들어지며 인스턴스와 다르게 여러 곳에서 공유하는 목적으로 사용된다.

 

✔️ 변수와 생명주기

지역변수(매개변수 포함): 스택 영영에 있는 스택 프레임 안에서 보관된다.

메서드가 종료되면 스택 프레임도 제거되는데 이때 해당 프레임에 포함된 지역 변수도 함께 제거된다.

인스턴스 변수: 인스턴스에 있는 멤버변수를 인스턴스 변수라고 한다. 인스턴스 변수는 힙 영역을 사용한다.

힙 영역은 GC가 발생하기 전까지느 생존하기 때문에 보통 지역변수보다 생명 주기가 길다.

클래스 변수: 메서드 영역의 static영역에 보관되는 변수이다.

메서드 영역은 프로그램 전체에서 사용하는 공용 공간으로 해당 클래스가 JVM에 로딩되는 순간 생성된다.

JVM이 종료될 때까지 생명주기가 이어져 가장 긴 생명주기를 가진다.

 

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

하지만 인스턴스를 통해 접근은 추천하지 않는다.

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

//클래스를 통합 접근
System.out.println(Data3.count);

 

 

static 메서드

public class DecoUtil1 {
    public String deco(String str) {
        String result = "*" + str + "*";
        return result;
    }
}

 

deco() 메서드를 호출하기 위해 DecoUtil1의 인스턴스를 먼저 생성해야 한다.

근데 deco() 기능은 멤버 변수도 없고, 단순히 기능만 제공할 뿐이다.

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

 

메서드에 static을 붙여 정적 메서드로 만든다.

그러면 이 정적 메서드는 정적 변수처럼 인스턴스 생성없이 클래스 명을 통해서 바로 호출할 수 있다.

public class DecoUtil2 {
    public static String deco(String str) {
        String result = "*" + str + "*";
        return result;
    }
}
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 메서드는 static만 사용할 수 있다.

클래스 내부의 기능을 사용할 때, 정적 메서드는 static이 붙은 정적 메서드나 정적 변수만 사용할 수 있다.

대신 모든 곳에서 static을 호출할 수 있다.

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);
    }
}
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();
    }
}
1.정적 호출
staticValue=1
2.인스턴스 호출1
instanceValue=1
staticValue=2
3.인스턴스 호출2
instanceValue=1
staticValue=3

 

이러한 정적 메서드는 객체 생성 없이 메서드 호출만으로 필요한 기능을 수행할 때 주로 사용된다.

예를 들어 간단한 메서드 하나로 끝나는 유틸리티성 메서드에서 자주 사용된다.

 

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

프로그램을 시작할 때 객체를 생성하지 않고 main()메서드가 동작하는 이유가 static이기 때문이다.

 


 

김영한, 실전 자바 기본편

https://inf.run/PuC6W