자바 메모리 구조와 static

고동현·2024년 6월 7일
0

JAVA

목록 보기
7/22

자바 메모리구조

  • 메서드영역
    프로그램을 실행하는데 필요한 공통 데이터 관리, 프로그램의 모든 영역에서 공유
    클래스 정보: 클래스의 실행코드, 필드, 메서드, 생성자등
    static영역: static 변수를 보관
    런타임 상수: 리터럴 상수

  • 스택영역: 각 스택프레임은 지역변수, 중간 연산 결과, 메서드 호출정보등이 있음
    메서드 종료시 스택 프레임 제거
    main부터 시작하니까 main frame이 제일 밑에 깔려있음

  • 힙영역: 객체와 배열 이 생성, GC에 의해서 사용되지 않는 개체 제거

참고: 스택영역은 각 쓰레드별로 각각 스택영역이 생성 -> 자세한 내용은 이 글 참고


100개의 인스턴스 생성 -> 힙에 100개의 인스턴스 생김
각각 내부 변수는 달라도 메서드는 공통된 코드 공유

x001.method()호출시 힙영역에서 해당 인스턴스를 찾는다.
모든 클래스 정보가 있는 메서드영역에서 해당 코드를 불러서 수행한다.

스택영역


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

    private static void method1(int i) {
        System.out.println("method1 start");
        int cal = i*2;
        method2(cal);
        System.out.println("method2 start");
    }

    private static void method2(int cal) {
        System.out.println("method2 start");
        System.out.println("method2 end");
    }
}

출력 로그

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

main이 먼저 시작해서 제일 나중에 끝난다.

호출

종료

스택영역과 힙영역

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

    private static void method1() {
        System.out.println("method1 start");
        Data data1 = new Data(10);
        method2(data1);
        System.out.println("method1 end");

    }

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

메모리 과정

처음에 main을 시작 -> 스택에 메인 프레임 생성

main에서 method1 호출 -> method1 스택 프레임 생성
method1메서드에서 Data 객체 생성 -> 참조값 지역변수로 stack에 저장 -> 힙에 인스턴스 생성

method1이 method2 호출 -> 항상 객체의 참조값을 넘기는것이 핵심
-> method2 의 지역변수 stack에 저장

method2종료되면서 스택프레임 제거, data2도 삭제

method1 종료되면서 스텍프레임 제거, data1 삭제

더이상 인스턴스를 참조하는곳이 없으면 GC가 더이상 사용하지 않는 인스턴스 삭제

static 변수

생성자를 100번 호출하면 각각 다른 인스턴스 100개가 힙영역에 생성된다.
만약에 객체를 몇개 생성했는지 count를 세고 싶다면 어떻게할까?
해당 객체에 count를 두면 해당 인스턴스의 count는 전부 별개의 필드이므로 셀수 없을 것이다.

public class Counter {
    public int count;
}
public class Data {
    public String name;
    public Data(String name,Counter counter){
        this.name = name;
        counter.count++;
    }
}
public class DataCountMain {
    public static void main(String[] args) {
        Counter counter = new Counter();
        Data data = new Data("A",counter);
        System.out.println("A count = "+counter.count);

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

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

이렇게하면 힙영역에 Counter인스턴스 1개, Data인스턴스 3개가 생성된다.

참고로 Data 생성자의 동일한 Counter의 참조값을 받았으므로 Counter객체의 필드 count는 공유가 된다.

이렇게 하면 문제점이, 다른 class를 추가해야 한다는 점이고, 생성자가 복잡해지고, Data클래스의 내부에서 해결하고싶다는 바램이 생긴다.

static 변수 사용

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("A count=" + Data3.count);

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


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

필드(멤버변수)의 종류

  • 인스턴스 변수
    static이 붙지 않은 멤버 변수
    인스턴스를 생성해야 사용
    인스턴스 변수를 만들때 마다 새로 만들어진다.
  • 클래스 변수
    static이 붙은 멤버변수
    클래스 변수, 정적 변수, static변수등으로 부름
    인스턴스와 무관하게 클래스에 바로 접근가능
    자바 프로그램을 시작할때 딱 1개가 만들어짐, 인스턴스와 다르게 여러곳에서 공유하는 목적으로 사용

변수와 생명주기

  • 지역변수(매개변수 포함)
    스택영역에 있는 스택 프레임에 보관
    메서드가 종료 -> 프레임 제거 -> 해당 프레임 내의 지역변수도 함께 제거
  • 인스턴스 변수
    힙 영역 사용 -> GC가 발생하기 전 까지 생존
  • 클래스 변수
    메서드 영역의 static 영역에서 보관
    해당 클래스가 JVM에 로딩이 되는순가 클래스 변수 생성
    JVM이 종료될때 까지 생명주기가 이어짐
    프로그램 실행 시점에 딱 만들어지고 종료시점에 제거

참고: data1.count로 인스턴스를 통한 점근이 가능 -> but 권장 x, 인스턴스 변수인지 착각가능

static 메서드

public class Deco {
    public static String deco(String s){
        return "*" + s + "*";
    }
}
public class DecoMain {
    public static void main(String[] args) {
        String s = "hello";
        System.out.println("Before deco: "+s);

        String deco = Deco.deco(s);
        System.out.println("After deco:" +deco);
    }
}

deco메서드는 정적 메서드이다. 인스턴스 생성없이 클래스 명으로 호출이 가능하다.
만약 정적 메서드가 아니라면, 메서드 호출을 위해서 항상 인스턴스를 생성하고 인스턴스를 통한 접근을 해야할 것이다.
정적 메서드 덕분에 불필요한 객체 생성 없이 편리하게 메서드를 사용했다.

JVM이 클래스를 로딩할때 해당 클래스가 메서드 영역에 올라가는데
클래스, 메서드등의 실행코드(static 아닌것) 메서드 영역에 올려둔다.
static이 붙으면, 메서드영역에서 따로 static영역에 메서드와 변수를 보관한다.

예제.

public class DecoData {
    private int instanceValue;
    private static int staticValue;
    public static void staticCall(){
        staticValue++;
        staticMethod();
    }

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

    public void instanceCall(){
        instanceValue++;
        instanceMethod();
        staticValue++;
        staticMethod();
    }

    private void instanceMethod() {
        System.out.println("instanceValue = "+instanceValue);
    }
}
public class DecoDataMain {
    public static void main(String[] args) {
        System.out.println("정적호출");
        DecoData.staticCall();

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

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

정적메서드에서 중요한점은 이거다.

  • static 메서드는 static만 사용할 수 있다.
    static이 붙은 정적메서드나 정적 변수만 사용이 가능하다.
    인스턴스 변수나 인스턴스 메서드는 사용이 불가하다.

  • 반대로 접근제어자만 허용한다면 모든 곳에서 static을 호출할 수 있다.

정적 메서드나 정적 변수를 사용하면 class명 . 으로 접근이 가능하다.
그러나 정적메서드 입장에서 인스턴스 변수나 메서드를 사용하려면, 해당 인스턴스의 참조값을 알아야하는데 알 방법이 없다. 고로 사용이 불가능 한 것이다.

참고로 당연히 data2.staticCall()과 같은 접근이 가능하나, 하지 않는게 좋다. 인스턴스메서드라고 오해할 수 있기 때문이다.

DecoData.staticCall();
DecoData.staticCall();
...
이렇게 자주쓰는건 DecoData.을 static import하면 편리하다.

main() 메서드는 정적 메서드
인스턴스 생성없이 실행되는 가장 대표적인 메서드가 바로 main메서드이다.
프로그램을 시작하는 시작점이 되는데 생각해보면 객체 생성없이 작동했다. 바로 static이기 때문이다.

따라서, 정적 메서드는 정적 메서드만 호출이 가능하므로, 정적메서드인 main이 호출하는 메서드에는 정적 메서드를 사용했다.

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

정적메서드인 main메서드가 같은 클래스에서 호출하는 메서드도 정적메서드로 선언해서 사용해야한다.

profile
항상 Why?[왜썻는지] What?[이를 통해 무엇을 얻었는지 생각하겠습니다.]

0개의 댓글