자바 메모리 구조 & static

2024. 2. 6. 12:24Java

◎ 자바의 메모리 구조

-> 자바 메모리 구조는 메서드 영역, 스택 영역, 힙 영역으로 나눌 수 있다.

-> 메서드 영역 : 프로그램을 실행하는데 필요한 공통 데이터를 관리한다. 해당 영역의 데이터는 프로그램의 모든 영역에서 공유한다.

 - 클래스 정보 : 클래스의 실행 코드(바이트 코드), 필드, 메서드, 생성자 등 모든 실행 코드가 존재한다.

 - static 영역 : static 변수를 보관하는 영역이다.

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

 

-> 스택 영역 : 자바 실행 시 스택이 생성된다. 각 스택 프레임은 지역 변수, 중간 연산결과, 메서드 호출 정보 등을 포함한다.

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

 

-> 힙 영역 : 인스턴스와 배열이 생성되는 영역이다. GC가 이루어지는 영역으로 더 이상 참조되지 않는 객체는 GC에 의해 제거된다.

 

-> 자바에서 특정 클래스로 인스턴스를 여러 개 생성하면 그 개수만큼 힙 영역에 인스턴스가 생성된다. 각 인스턴스는 내부에 변수와 메서드를 가진다. 같은 클래스로부터 생성된 인스턴스라도 인스턴스 내부의 변수 값은 다를 수 있지만 메서드는 공통된 코드를 공유한다. 따라서 인스턴스가 생성되면 인스턴스 변수에는 메모리가 할당되지만, 메서드에 대한 새로운 메모리 할당은 발생하지 않고, 메서드는 메서드 영역에서 공통으로 관리되고 실행된다.

 

◎ 스택 영역

-> 코드를 통해 스택 영역에 대해 알아본다.

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

-> 결과

-> 위의 코드의 실행 흐름은 다음과 같다.

main()메서드 -> method1()호출 -> method2() 호출 -> method2() 종료 -> method1()종료 -> main() 종료

-> 처음 자바 프로그램을 실행하면 main()메서드를 실행한다. 이 때 main()메서드를 위한 스택 프레임이 스택 영역에 생성된다.

-> main()에서 method1()을 호출한다. 이 때 스택 영역에 method1() 스택 프레임이 생성된다.

-> method1()은 cal, m1이라는 지역 변수를 가지고 있기 때문에 해당 지역 변수들이 스택 프레임에 포함된다.

-> method1()에서 method2()를 호출한다. 이 때 스택 영역에 method2() 스택 프레임이 생성된다.

-> method2()는 m2 지역 변수를 가지고 있기 때문에 해당 지역 변수가 스택 프레임에 포함된다.

-> method2()까지 호출되어 실행이 완료되면 스택의 최상단에 있던 method2()가 종료되면서 스택에서 제거된다.

-> method2()가 제거되면 그 아래에 있던 method1() 스택 프레임이 제거된다.

-> 이후 main()이 종료된고, 스택 영역에서 제거된다. 이제 스택 영역에는 아무것도 없다. 자바는 프로그램을 종료한다.

-> 스택 영역에 스택 프레임은 호출되는 순서대로 스택 영역에 쌓이고 종료되면 호출되는 순서의 역순으로 스택 영역에서 제거된다.

 

◎ 스택 영역과 힙 영역

-> 다음은 스택 영역과 힙 영역이 함께 사용되는 경우다.

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);
        method2(data1);
        System.out.println("method1 end");

    }

    static void method2(Data data2) {
        System.out.println("method2 start");
        System.out.println("data2.value = " + data2.getValue());
        System.out.println("method2 end");
    }
}

-> 결과

-> main() -> method1() -> method2() 순서로 호출되는 코드다.

 

-> main()메서드가 호출이 되고 main() 스택 프레임이 스택 영역에 생성된다.

-> main()에서 method1()을 호출하고, 스택 영역에는 method1() 프레임이 생성된다.

-> method1() 스택 프레임은 지역 변수 Data data1을 포함한다.

-> method1()에서는 new Data(10);를 통해 힙 영역에 Data 인스턴스를 생성한다. new 를 통해 생성된 Data 인스턴스의 참조값은 data1에 보관한다.

-> method1()에서 method2()를 호출하면서 매개변수로 new Data(10)를 통해 반환된 x001 참조값을 넘긴다.

-> method1()과 method2()에서는 같은 Data 인스턴스를 참조한다.

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

-> method1()도 종료되면서 method2()가 종료될 때 처럼 스택 프레임이 제거되면서 data1이 제거된다.

-> method1() 종료 이후에는 더 이상 x001 위치의 Data 인스턴스를 참조하는 곳이 없다.

-> 더 이상 참조하는 곳이 없어진 x001의 Data인스턴스는 GC 대상이 되어 메모리에서 제거된다.

 

static 변수

-> 아래 코드는 인스턴스가 생성된 수를 세는 프로그램이다.

package static1;

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

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

-> 결과

-> new를 통해 Data 인스턴스가 총 세 번 만들어졌기 때문에 기대하는 결과는 1,2,3이 나와야한다. 그런데 1,1,1이 나왔다.

-> 객체 생성 수를 카운트하는 변수 count는 Data1 클래스의 멤버 변수고, new를 통해 인스턴스를 생성할 때 마다 새로운 참조값 위치의 인스턴스가 생성되니 멤버 변수 count도 당연히 각 인스턴스에 독립적으로 존재한다. 따라서 위와 같은 결과가 나온 것이다.

-> 원하는 결과를 받으려면 count 변수를 인스턴스끼리 공유해서 사용해야 한다.

-> 이 때 static키워드를 사용하면 공용으로 사용하는 변수를 만들 수 있다.

-> 다음은 count를 static으로 선언하여 수정한 코드다.

package static1;

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

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

-> 결과

-> 원하는 결과가 나왔다.

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

-> Data3("A") 인스턴스를 생성하면 생성자가 호출되고, 생성자 내부의 count++ 코드가 실행되는데 이 count는 static이 붙은 정적 변수로 메서드 영역 내부에 존재한다. 참조값 위치마다 존재하는 변수가 아니라 메서드 영역 내에서 사용되는 변수로 이후에 Data3("B"), Data3("C") 인스턴스가 생성되어도 count는 메서드 영역에 있는 count를 공유해서 사용한다.

-> 또한 static이 붙은 변수는 '클래스.static변수'로 외부에서 접근한다.

 

※ 멤버 변수의 종류

- 인스턴스 변수 : static이 붙지 않은 변수. 인스턴스 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 존재한다. 인스턴스를 생성할 때 마다 새롭게 만들어진다.

 

- 클래스 변수 : static이 붙은 변수 이전 코드에서 static int count로 선언된 count가 클래스 변수다. 클래스 변수, 정적 변수, static 변수로 불린다. 인스턴스와 무관하게 클래스에 바로 접근하여 사용할 수 있다. 클래스에 소속되어 있다.

클래스 변수는 자바 프로그램을 실행할 때 1개가 만들어진다. 여러 곳에서 공유하는 목적으로 사용된다.

 

※ 변수의 생명주기

- 지역 변수(매개변수 포함) : 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관된다. 메서드 종료 시 스택 프레임이 제거되고 이 때 스택 프레임 내에 있던 지역변수도 제거된다.

 

- 인스턴스 변수 : 인스턴스 변수는 힙 영역에 있다. 힙 영역은 GC가 발생하기 전까지 생존한다.

 

- 클래스 변수 : 메서드 영역의 static 영역에 있다. 메서드 영역은 프로그램 전체에서 사용하는 공용 공간이다. 클래스 변수는 해당 클래스가 JVM에 로딩되는 순간 생성되고, JVM이 종료될 때 제거된다. 가장 긴 생명주기를 가진다.

프로그램 실행 시점에 만들어져 프로그램 종료 시 제거되기 때문에 동적이 아닌 정적 변수라 한다.

 

※ 정적 변수 접근 법

- 기본적으로 정적 변수 사용 시 정적 변수가 위치한 클래스에 직접 접근한다. 

Data3.count

 

- 이 방법 외에도 인스턴스를 통해 접근할 수도 있다.

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

 

static 메서드

-> static이 붙은 메서드다.

-> 아래 코드는 문자열을 받으면 문자열 앞 뒤에 *을 붙이는 코드다.

package static2;

public class DecoUtil {
    public String deco(String str) {
        return "*" + str + "*";
    }
}
package static2;

public class DecoMain1 {
    public static void main(String[] args) {
        String s = "hello";

        DecoUtil utils = new DecoUtil();
        String deco = utils.deco(s);

        System.out.println(deco);
    }
}

-> 결과

-> DecoUtil 클래스를 보면 인스턴스 변수가 없다. 단지 deco()메서드의 기능을 제공하기 위해 DecoMain1 클래스에서 DecoUtil인스턴스를 생성해서 사용했다. 

-> 인스턴스 변수를 사용하지 않기 때문에 인스턴스를 생성하는 의미가 없다.

-> deco()메서드에 static을 붙여서 코드를 수정해본다.

package static2;

public class DecoUtil2 {
    public static String deco(String str) {
        return "*" + str + "*";
    }
}
package static2;

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

        System.out.println(deco);
    }
}

-> deco()메서드에 static을 붙여 static 메서드로 만들었다. 이 메서드는 이제 메서드 영역에서 관리된다.

-> 이제 deco()메서드는 인스턴스를 통해서가 아니라 deco()메서드가 있는 클래스에 직접 접근하여 호출하면 된다. 더 이상 사용하지도 않는 인스턴스를 생성할 필요가 없다.

-> 이렇게 메서드 앞에 static이 붙은 메서드를 정적 메서드 또는 클래스 메서드라 한다.

 

※ 정적 메서드 호출 범위

- static 메서드는 static만 사용할 수 있다. 인스턴스 생성 없이 사용할 수 있기 때문에(참조값 없이 호출되기 때문에 참조값을 알아야하는 인스턴스 변수, 메서드 사용 불가) 인스턴스 생성을 통해 사용하는 인스턴스 변수와 인스턴스 메서드는  사용할 수 없다.

- 정적 메서드, 변수는 메서드 영역에서 관리된다. 메서드 영역의 데이터는 프로그램의 모든 영역에서 공유한다고 했다. 따라서 정적 메서드, 변수도 프로그램의 모든 영역에서 호출할 수 있다.

public class DecoData {
    private int instanceValue; // 인스턴스 변수
    private static int staticValue; // 정적 변수

    // static 메서드
    public static void staticCall(){
        staticValue++; // 정적 변수 접근 가능
        staticMethod(); // 정적 메서드 접근 가능
//        instanceValue++; // 인스턴스 변수 접근 불가
//        instanceMethod(); // 인스턴스 메서드 접근 불가
    }

    public void instanceCall(){
        instanceValue++; // 인스턴스 변수 접근 가능
        instanceMethod(); // 인스턴스 메서드 접근 가능
        staticValue++; // 정적 변수 접근 가능
        staticMethod(); // 정적 메서드 접근 가능
    }

    private void instanceMethod(){
        System.out.println("instanceValue = " + instanceValue);
    }

    private static void staticMethod(){
        System.out.println("staticValue = " + staticValue);

    }
}

 

 

 

 

'Java' 카테고리의 다른 글

상속  (0) 2024.02.08
final  (0) 2024.02.07
접근 제어자  (0) 2024.02.05
생성자  (0) 2024.02.01
객체 지향 프로그래밍  (0) 2024.01.31