2.7~8.Java 자바 메모리 구조와 static, final

sein lee·2025년 1월 16일
0

java-study

목록 보기
13/13

자바 메모리 구조와 static

자바 메모리 구조

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

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

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

스택과 큐 자료구조

  • 후입 선출(LIFO, Last In First Out) : 여기서 가장 마지막에 넣은 3번이 가장 먼저 나온다. 이렇게 나중에 넣은 것이 가장 먼저 나오는 것을 후입 선출이라 하고, 이런 자료 구조를 스택이라 한다.
  • 선입 선출(FIFO, First In First Out) : 후입 선출과 반대로 가장 먼저 넣은 것이 가장 먼저 나오는 것을 선입 선출이라 한다. 이런 자료 구조를 큐(Queue)라고 한다.

스택영역

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

}


-> 스택 구조

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

스택 영역과 힙 영역

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

}

  • method1에서 Data 클래스의 인스턴스 생성
  • method1() 에서 method2() 를 호출할 때 매개변수에 Data 인스턴스의 참조값을 전달
  • method1() 은 지역 변수로 Data data1 을 가지고 있다. 이 지역 변수도 스택 프레임에 포함된다.
  • method1() 은 new Data(10) 를 사용해서 힙 영역에 Data 인스턴스를 생성한다. 그리고 참조값을 data1에 보관한다.
  • method1() 은 method2() 를 호출하면서 Data data2 매개변수에 x001 참조값을 넘긴다.
  • 이제 method1() 에 있는 data1 과 method2() 에 있는 data2 지역 변수(매개변수 포함)는 둘다 같은 x001 인스턴스를 참조한다.

지역 변수는 스택 영역에, 객체(인스턴스)는 힙 영역에 관리되는 것을 확인했다. 이제 나머지 하나가 남았다. 바로 메서드 영역이다. 메서드 영역이 관리하는 변수도 있다. 이것을 이해하기 위해서는 먼저 static 키워드를 알아야 한다. static 키워드는 메서드 영역과 밀접한 연관이 있다.

static 변수1

생성된 데이터들의 갯수세기

인스턴스 내부 변수에 카운트 저장

<Data1.java>

package static1;

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

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

<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 는 계속 초기화된다.)
=>인스턴스에 사용되는 멤버 변수 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("B",counter);
        System.out.println("B count="+ counter.count);

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


-> Data2의 인스턴스는 3개가 생성
=> 하지만 Data2 클래스와 관련된 일인데, Counter 라는 별도의 클래스를 추가로 사용해야 하고, 생성자 매개변수 추가로 생성자와 생성자 호출부분이 복잡해진다.
=> 외부의 도움을 받지 않는 방법?(Counter 메서드를 따로 만들지 않는 법?) =static

static 변수2

특정 클래스에서 공용으로 함께 사용할 수 있는 변수를 만들 수 있다면 편리할 것이다.
static 키워드를 사용하면 공용으로 함께 사용하는 변수를 만들 수 있다.
<Data3.java>

package static1;

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

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

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

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

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

-> Data3.count 는 마치 클래스에 직접 접근하는 것 처럼 느껴짐

  • static 이 붙은 멤버 변수는 메서드 영역에서 관리한다.
    static 이 붙은 멤버 변수 count 는 인스턴스 영역에 생성되지 않는다. 대신에 메서드 영역에서 이 변수를 관리한다.
  • Data3("A") 인스턴스를 생성하면 생성자가 호출된다.
  • 생성자에는 count++ 코드가 있다. count 는 static 이 붙은 정적 변수다. 정적 변수는 인스턴스 영역이 아니라 메서드 영역에서 관리한다. 따라서 이 경우 메서드 영역에 있는 count 의 값이 하나 증가된다.

=>static 변수는 쉽게 이야기해서 클래스인 붕어빵 틀이 특별히 관리하는 변수이다. 붕어빵 틀은 1개이므로 클래스 변수도 하나만 존재한다. 반면에 인스턴스 변수는 붕어빵인 인스턴스의 수 만큼 존재한다.

static 변수 3

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

    public Data3(String name){
        this.name = name;
        count++;
    }
}
  • name.count -> 멤버변수(필드)

멤버변수는 static의 유무로 두가지로 나뉨

  • 인스턴스 변수 : static이 붙지 않은 변수 예)name
    - static 이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있다. 따라서 인스턴스 변수라 한다.
    - 인스턴스 변수는 인스턴스를 만들 때 마다 새로 만들어진다.
  • 클래스 변수 : static 이 붙은 멤버 변수, 예) count
    - 클래스 변수, 정적 변수, static 변수 등으로 부른다. 용어를 모두 사용하니 주의하자!
    - static 이 붙은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있다. 따라서 클래스 변수라 한다.
    - 클래스 변수는 자바 프로그램을 시작할 때 딱 1개가 만들어진다. 인스턴스와는 다르게 보통 여러곳에서 공유하는 목적으로 사용된다.

변수와 생명주기

  • 지역변수 : 스택영역에 생성, 메서드가 종료되면 스택 프레임이 제거되면서 지역변수도 같이 제거
  • 인스턴스 변수 : 힙 영역에 생성, GC가 발생하기 전까지만 생존
  • 클래스 변수 : 메서드 영역에 생성, JVM 이 종료되기 전까지 생존

static 은 정적변수다. 왜냐하면 인스턴스변수는 힙영역에서 동적으로 제거되고 생성되지만 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);

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

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

인스턴스를 통한 접근은 권장하지 않는다.
왜냐? 가져다 쓰는 입장에서 data4.count는 인스턴스변수인가? 하는 의문이 들 수 있지만 Data3.count는 누가봐도 static 이네!가 가능해서..

static 메서드1

인스턴스 메서드

<DecoUtil1.java>

package static2;

public class DecoUtil1 {
    public String deco(String str){ //deco 메서드는 멤버변수도 없고 단순히 기능만 제공
        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() 메서드는 멤버변수도 없고 기능만 제공하는 메서드이다. 그런데 굳이 Main 코드에서 정의하고 호출까지 해야할까?

static 메서드

<DecoUtil2.java>

package static2;

public class DecoUtil2 {
    public static String deco(String str){ //정적 메서드 : 인스턴스 생성 없이 클래스로 바로 호출 가능
        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); //static 때문에 바로 호출 가능

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

    }
}

메서드에 static이 붙음으로써 정적메서드가 된다. 이렇게 하면 인스턴스 생성없이 클래스명으로 바로 호출이 가능하다. -> 불필요한 객체생성이 없어짐

  • static이 붙은 메서드를 정적 메서드 or 클래스메서드라 한다.
  • static이 붙지 않은 메서드는 인스턴스 메서드

static메서드2

정적메서드 사용법

  • static은 static만 사용가능
    * 정적 메서드는 static이 붙은 정적메서드나 정적변수만 사용할 수 있다.
    • 인스턴스는 불가능
  • static은 어느곳에서도 호출이 가능

<DecoData.java>

package static2;

public class DecoData {
    private int instanceValue;
    private static int staticValue;

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

        staticValue++; //정적 변수에 접근, 호출 가능
        staticMethod();

        //static 은 static만 호출가능
    }

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

        staticValue++;
        staticMethod();

        //instance는 instance, static 호출가능
    }

    public static void staticCall(DecoData data){
        data.instanceValue++;
        data.instanceMethod();
        //static에서 instance 호출 가능하지만 여기 있는 data는 외부에 생긴 새로운 참조값
    }

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

<DecoDataMain.java>

package static2;

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

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

        System.out.println("3.인스턴스 호출-정적");
        DecoData.staticCall();

        System.out.println("3.정적 호출-외부참조");
        DecoData.staticCall(decoData);
    }
}
  • 정적-> 정적
    => 정적 메서드는 클래스 이름을 통해 바로 호출, 인스턴스처럼 참조값이 존재하지 않는다.
    BUT 객체의 참조값을 매개변수로 전달하면 인스턴스 호출 가능(외부에서 참조값을 생성하기때문에..ex.3.정적 호출-외부참조)
  • 인스턴스 -> 정적 & 인스턴스

static메서드3

용어 정리

  • 인스턴스 메서드: static이 붙지 않은 메서드
  • 클래스 메서드: static이 붙은 메서드 - 정적메서드, static메서드 등으로 불림

정적 메서드 활용

객체 생성이 필요 없이 메서드 호출만으로 필요한 기능을 수행할 때 사용, 유틸리티성 메서드

정적메서드 접근법


둘다 결과적으로는 정적 메서드에 접근하지만 위의 것은 코드를 읽을 때 마치 인스턴스메서드에 접근하는 것처럼 오해 할 수 있음

static import

정적메서드를 자주 사용한다면 import 기능 사용

Alt+Enter

import static static2.DecoData.*; // 모든 정적메서드에 적용

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

정적메서드인 main()메서드가 같은 클래스에서 호출하는 메서드도 정적메서드로 선언해야함.

package static2;

import oop1.ValueData;

public class ValueDataMain {
    public static void main(String[] args) { //static이기 때문에
        ValueData valueData = new ValueData();
        add(valueData);
    }
    static void add(ValueData valueData) {//static 으로 선언
        valueData.value++;
        System.out.println("숫자 증가 value=" + valueData.value);
    }
}

final

final 변수와 상수1

final은 이름 그대로 끝! 이라는 뜻, 변경이 불가능

package final1;

public class FinalLocalMain {
    public static void main(String[] args) {
        //final 지역변수
        final int data1;
        data1 = 10; //최초 한번만 할당 가능
//        data2 = 20; //컴파일 오류

        //final 지역변수2
        final int data2=10;
//        data2 =20;//컴파일 오류

        method(10);
    }
    static void method(final int parameter){
//        parameter = 10; //컴파일 오류 , 메서드 내에서 매개변수를 줄 수 없다.

    }
}
  • final 을 지역변수에 설정할 경우 최초 한번만 할당 가능
  • 매개변수에 final이 붙으면 메서드 내부에서 매개변수의 값을 변경할 수 없다.
package final1;

public class ConstructInit {
    final int value; // 이것만 하면 에러 남 -> 아래처럼 생성자를 통해서 넣어야함

    public ConstructInit(int value){
        this.value = value;
    }
}
  • final을 필드에 사용하면 해당 필드는 생성자를 통해서 한번만 초기화 될 수 있다.
package final1;

public class FieldInit {
    static final int CONST_VALUE = 10; //static final 대문자 -> 관례

//    final int value = 10; // 초기 값을  경우에는 아래처럼 생성자를 통해서 할당이 불가
//    public FieldInit(int value){
//        this.value = value;
//    }

    final int value = 10;

}
  • final 필드를 필드에서 초기화하면 이미 값이 설정되었기 때문에 생성자를 통해서도 초기화 할 수 없다
profile
개발감자

0개의 댓글

관련 채용 정보