공부/Java

자바 메모리 구조와 static

Stair 2024. 9. 2. 11:07
반응형

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

1. 메서드 영역 : 클래스 정보를 보관한다. 이 클래스 정보가 붕어빵 틀이다.

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

3. 힙 영역 : 객체(인스턴스)가 생성되는  영역이다. new 명령어를 사용하면 이 영역을 사용한다. 쉽게 이야기해서 붕어빵 틀로부터 생성된 붕어빵이 존재하는 공간이다. 참고로 배열도 이 영역에 생성된다.

 

 

 

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

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

2. static 영역 : static 변수들을 보관한다.

3. 런타임 상수 풀 : 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관한다. 예를 들어서 프로그램에 "hello"라는 리터럴 문자가 있으면 이런 문자를 공통으로 묶어서 관리한다. 외에도 프로그램을 효율적으로 관리하기 위한 상수들을 관리한다.

 

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

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

 

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

 

**참고 : 스택 영역은 각 쓰레드별로 하나의 실행 스택이 생성된다. 따라서 쓰레드 수 만큼 스택 영역이 생성된다. 지금은 쓰레드를 1개만 사용하므로 스택 영역도 하나이다. 쓰레드에 대한 부분은 멀티 쓰레드를 학습해야 이해할 수 있다.

 

 

Stack ,Queue 자료 구조

 

스택은 통 하나에 순서대로 넣고 빼는 구조이다.

LIFO(Last In First Out) 구조라고 볼 수 있다.

 

큐는 빨대(?)를 생각하면 될 거 같다

FIFO(First In First Out) 구조를 지니고 있다.

 

이러한 자료 구조는 각자 필요한 영역이 있다. 예를 들어 선착순 이벤트를 하는데 Stack구조를 사용한다면 문제가 발생할 것이다. 늦게 온 사람이 먼저 빠져버리기 때문이다.

public static void main(String[] args) {
    System.out.println("start");
    method1(10);
    method2(20);

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

 

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

 

위와 같은 코드와 결과를 보자

스택 프레임에 main() 메서드가 적재된 후 method1을 호출한다. methd1이 돌아가는 도중 method2를 호출하고 method2가 종료되면서 스택프레임에서 빠져나간다. method1도 종료된 뒤 스택프레임에서 빠져나가고 마지막으로 main()이 종료된다.

 

위와 같은 구조를 스택이라고 한다.

 

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

2. 메서드를 계속 호출하면 스택 프레임이 쌓인다.

3. 지역변수는 스택 영역에서 관리한다.

4. 스택 프레임이 종료되면 지역 변수도 함께 제거된다.

5. 스택 프레임이 모두 제거되면 프로그램도 종료된다.

 

public class Data {
    private int value;

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

    public int getValue(){
        return value;
    }
}

 

 

public static void main(String[] args) {
    System.out.println("main start");
    method1();
}

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

 

위와 같은 예제를 돌려보면 stack에 대해 좀 자세히 이해할 수 있다

 

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

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

    public Data1(String name){
        this.name = name;
        count++;
    }
}
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="+data1.count);
    Data1 data3 = new Data1("C");
    System.out.println("C count="+data1.count);
}

 

위와 같은 예제를 출력하면 결과값은

A count=1
B count=1
C count=1

 

이 나타나게 된다. 왜 카운트가 3이 되지 않고 1이 세개가 출력 되나.

이유는 간단하다. 객체를 총 세개가 생성되었기 때문이다.

 

인스턴스에 사용되는 멤버 변수 count 값은 인스턴스 끼리 서로 공유되지 않는다. 이 문제를 해결하려면 아래와 같이 구현해야한다.

public class Counter {
    public int count;
}
public class Data2 {
    public String name;

    public Data2(String name, Counter counter){
        this.name = name;
        counter.count++;
    }
}
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("A", counter);
    System.out.println("C count=" + counter.count);

}

 

결과적으로 Data2의 인스턴스가 세개가 생성 되고 count 인스턴스도 생성되어 숫자 3이 출력되게 된다.

그러나 이 코드는 Counter 라는 클래스를 하나 만들고 인스턴스도 생성해야하는 번거로움이 있다.

 

이를 해결하기 위해 자바에서는 static 키워드를 제공해준다. static 키워드를 사용하면 공용으로 함께 사용하는 변수를 만들 수 있다.

public class Data3 {

    public String name;
    public static int count;

    public Data3(String name){
        this.name = name;
        count++;
    }

}

 

public static int cout 부분을 보면 변수 타입 앞에 static 키워드가 붙었다.

이렇게 멤버 변수에 static을 붙히게 되면 static 변수 또는 정적 변수 or 클래스 변수라고 한다.

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

    System.out.println("Data3.count = " + Data3.count);
    
}

 

이전 코드와는 달리 객체의 주소를 통해 접근하는 것이 아닌 클래스에 직접 접근하는 것 처럼 느껴진다.
static이 붙은 정적 변수에 접근하려면 Data3.count와 같이 클래스명 + . + 변수명으로 접근하면 된다.

Data3의 

 

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

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

 

static 변수는 쉽게 이야기해서 클래스인 붕어빵 틀이 특별하게 관리하는 변수이다. 붕어빵 틀은 한개이므로 클래스 변수도 하나만 존재한다. 반면에 위와 같이 data1,  data2, data3과 인스턴스의 붕어빵은 인스턴스의 수 만큼 변수가 존재한다.

 

public class Data3 {

    public String name;
    public static int count;

    public Data3(String name){
        this.name = name;
        count++;
    }

}

 

위 코드에서 count는 Data3의 멤버변수이자 클래스 변수이다. 클래스 변수는 static이 붙은 멤버 변수이다.

name 같은 경우는 Data3의 멤버변수이자  인스턴스 변수라고 한다. 인스턴스 변수는 인스턴스를 만들 때 마다 새로 만들어진다.

왜 이름이 static일까? 힙 영역에 생성되는 인스턴스 변수는 동적으로 생성되고 제거된다.

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

 

변수의 생명 주기

지역 변수(매개변수 포함) -> 인스턴스 변수 -> 클래스 변수

 

 

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

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

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

 

인스턴스를 통해 접근할수도있지만, 실제로는 인스턴스에 있는게 아니고 인스턴스를 들렀다가 위치 확인 후 클래스 영역에 있는 count에 접근한다.

**인스턴스를 통한 접근은 권장하지 않는다. static변수가 인스턴스에 있는 인스턴스 변수처럼 보여지기 때문이다.

**단 자기 클래스 내부에서는 Data3.count처럼 접근하지 않아도 된다.

 

이런 static은 메서드에도 적용이 될 수 있는데,

public class DecoUtil2 {

    public static String deco(String str){
        String result = "*" +str + "*";
        return result;
    }
}
public static void main(String[] args) {
    String s = "Hello java";
    String deco = DecoUtil2.deco(s);


    System.out.println("s = " + s);
    System.out.println("deco = " + deco);
}

 

static이 붙은 정적 메서드는 객체 생성 없이 클래스명 + . + 메서드 명으로 바로 호출할 수 있다.

정적 메서드 덕분에 불필요한 객체 생성 없이 편리하게 메서드를 사용한 것이다.

 

이러한 메서드를 정적(static) 메서드 또는 클래스 메서드라고 한다.

static이 붙지 않은 메서드는 인스턴스를 생성해야 호출할 수 있다. 이것을 인스턴스 메서드라고 한다.

 

정적 메서드는 객체 생성 없이 클래스에 있는 메서드를 바로 호출할 수 있다는 장점이 있지만, 언제나 사용할 수 있는 것은 아니다.

1.static 메서드는 static만 사용할 수 있다.(스태틱 본인은 스태틱이 붙은것만 호출할 수 있다)

2. 반대로 모든곳에서 static을 호출할 수 있다.

 

package static2;

public class DecoData {
    private int instanceValue;

    private static int staticValue;

    public static void staticCall(){
//        instanceValue++;
//        instanceMethod();

        staticValue++;
        staticMethod();
    }

    public void instanceCall(){
        instanceValue++;
        staticMethod();

        staticValue++;
        staticMethod();

    }

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

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

 

    public static void main(String[] args) {
//        DecoData data = new DecoData();

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

    }

 

위 예제 staticCall() 메서드를 보면 인스턴스 변수와 인스턴스 메서드는 접근이 불가능하다.

반면 instanceMethod() 메서드는 인스턴스 변수, 인스턴스 메서드, 정적 변수, 정적 메서드 모두 접근이 가능하다.

 

정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있다. 그래서 인스턴스처럼 참조값의 개념이 없다. 특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데, 정적 메서드는 참조값 없이 호출한다(객체가 생성될때 생성되는 것이 아닌 클래스 필드에 따로 생성되어져 있기 때문). 따라서 정적 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없다.

 

또한

import static static2.DecoData.*;

를 임포트하여 클래스명을 생략하여 코드를 작성할 수 있다.

import static static2.DecoData.*;

public class DecoDataMain {
    public static void main(String[] args) {
//        DecoData data = new DecoData();

        System.out.println("1. 정적 호출");
        staticCall();

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


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

       DecoData data3 = new DecoData();
       data3.staticCall();
       staticCall();

    }
}

 

 

마지막으로 main()메서드는 정적 메서드이다.

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

 

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

반응형

'공부 > Java' 카테고리의 다른 글

자바 상속이란  (1) 2024.09.03
자바 final  (1) 2024.09.02
자바 접근 제어자  (5) 2024.08.31
자바 패키지  (0) 2024.08.31
자바 생성자  (1) 2024.08.30