[Java 기본]Section 7 - 자바 메모리 구조

우영·2024년 2월 27일

JAVA 기본편

목록 보기
7/13

자바 메모리 구조 - 비유

JVM

JVM 이란 Java Virual Machine의 약자이며, 가상 자바 머신이라고 불린다. Java의 바이트 코드를 해석하고 실행하는 역할을 한다.

JVM 실행 순서
1. java 파일을 컴파일러를 통해 .class 파일로 변환한다.
2. class 파일을 JVM의 ClassLoader에게 보낸다.
3. 클래스로더에서 JVM 런타임 영역으로 로딩하여 메모리에 올린다.
jvm이 메모리 구조를 내부에서 가지고 동작.

비유


크게 3가지

  • 메서드 영역: 클래스 정보를 보관한다. 예) 붕어빵 틀
  • 스택 영역: 실제 프로그램이 실행되는 영역. 메서드를 실행할 때 마다 하나씩 쌓임
  • 힙 영역: 객체가 생성되는 영역. new 명령어를 사용하면 이 영역을 사용. 쉽게 얘기해서 붕어빵 틀로부터 생성된 붕어빵이 존재하는 공간 참고로 배열도 new로 만듦으로 이 영역에 해당

실제

  • 메서드 영역: 프로그램을 실행하는데 필요한 공통 데이터 관리. 이 영역은 프로그램의 모든 영역에서 공유
    • 클래스 정보: 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재
    • static 영역: static 변수들을 보관. 뒤에서 설명
    • 런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관한다. 예를 들어서 프로그램에 "hello"라는 리터럴 문자가 있으면 이런 문자를 공통으로 묶어서 관리. 이 외에도 효율적으로 관리하기 위한 상수 관리

  • 스택 영역: 자바 실행 시, 하나의 실행 스택이 생성된다. 각 스택 프레임은 지역변수, 중간 연산 결과, 메서드 호출 정보 등을 포함
    • 스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이다. 메서드를 호츌할 때마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거됨.

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

GC(가비지 컬렉션) : heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객체를 모아 주기적으로 제거하는 프로세스

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

스택과 큐 자료구조


스택구조와 큐(queue) 자료 구조

  • 스택구조: 후입 선출(LIFO,Last In First Out) 나중에 넣은 것이 가장 먼저 나오는 것, 이런 구조를 스택이라 부른다.
  • 큐 구조: 선입선출(First In First Out) 처음에 넣은 것이 가장 먼저 나오는 것, 이런 구조를 큐라고 부른다. 예) 선착순 이벤트(가장 먼저 들어온 고객이 가장 먼저 당첨.)

하지만 프로그램 실행과 메서드 호출에는 스택 구조가 적합.

스택 영역

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 start
method1 start
method2 start
method2 end
method1 end
main end

시작
1. 자바 프로그램을 처음 실행하면 main()을 실행. 이때 main()을 위한 스택 프레임이 하나 생성된다.

  • main() 스택 프레임은 내부에 args라는 매개변수를 가진다. args는 뒤에서 다룬다.
  1. main()은 method1()을 호출한다. method1() 스택 프레임이 생성된다.
  2. method1()은 method2()을 호출한다. method2() 스택 프레임이 생성된다. method2()는 m2 지역 변수를 가지므로 해당 지역 변수가 스택 프레임에 포함된다.

종료
1. method()가 종료된다. 스택 프레임이 제거되고, 매개변수 m2도 제거된다. method2 스택 프레임이 제거 되었으므로 프로그램은 method1()로 돌아간다.
2. method1()이 종료된다. method() 스택 프레임이 제거되고, 지역 변수 m1, cal도 제거된다. 프로그램은 main()으로 돌아간다.

정리

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

스택 영역과 힙 영역


Data

package memory;
public class Data {
 private int value;
 public Data(int value) {
 this.value = value;
 }
 public int getValue() {
 return value;
 }
}

JavaMemoryMain2

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

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

method1()에서 Data 클래스의 인스턴스를 생성

  • method()은 method()를 호출하면서 Data data2 매개변수에 x001 참조값을 넘긴다.
  • mehtod1()에 있는 data1과 method2()에 있는 data2 지역 변수는 둘다 같은 x001 인스턴스를 참조한다.

참고: 힙 영역 외부가 아닌, 힙 영역 안에서만 인스턴스끼리 서로 참조하는 경우애도 GC의 대상이 된다.

static 변수1(정적 변수)


정의: 메모리에 한번 할당되어 프로그램이 종료될 때 해제되는 것


static 키워드는 주로 멤버 변수와 메서드에 사용된다.
왜 필요한지?

특정 클래스를 통해서 생성된 객체의 수를 세는 단순한 프로그램이다.

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

Data1

package static1;
public class Data1 {
 public String name;
 public int count;
 public Data1(String name) {
 this.name = name;
 count++;
 }
}
  • 생성된 객체의 수를 세어야 한다. 따라서 객체가 생성될 때마다 생성자를 통해 인스턴스의 멤버 변수인 count 값을 증가시킨다.
    예제를 단순하게 만들기 위해 필드에 public을 사용했다.

DataCountMain1

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);
 }
}
  • 객체를 생성하고 카운트 값을 출력한다.

정리: 인스턴스를 생성하면 이전 인스턴스는 관계없는 새로운 인스턴스를 생성한다. 인스턴스에 사용되는 멤버 변수 count 값은 인스턴스끼리 서로 공유되지 않는다.

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

Data2

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

DataCountMain2

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


Static 변수2 등장

Data1

package static1;
public class Data3 {
 public String name;
 public static int count; //static
 public Data3(String name) {
 this.name = name;
 count++;
 }
}
  • 기존 코드를 유지하기 위해 새로운 클래스 Data3를 만들었다.
  • static int count 부분을 보면, 변수 타입 앞에 static 키워드가 붙어 있다.
  • 멤버 변수 앞에 static을 붙이게 되면 static 변수, 정적 변수 또는 클래스 변수라 한다.
  • 객체가 생성되면 생성자에서 정적 변수 count 값을 하나 증가시킨다.

DataCountMain3

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

정적 변수에 접근하는 방법 = 클래스명에 .(dot)을 사용한다. 마치 클래스에 직접 접근하는 것처럼 느껴짐.

정리: static 변수는 클래스인 붕어빵 틀이 특별히 관리하는 변수이다. 붕어빵 틀은 1개이므로 클래스 변수도 하나만 존재한다. 반면에 인스턴스 변수는 붕어빵인 인스턴스의 수 만큼 존재한다.

용어 정리

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

예제 코드에서 name , count 는 둘다 멤버 변수이다.
멤버 변수(필드)는 static 이 붙은 것과 아닌 것에 따라 다음과 같이 분류할 수 있다.

멤버 변수의 종류

  • 인스턴스 변수 : static이 붙지 않은 멤버 변수 예) name

    • static이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속.
    • 인스턴스 변수는 만들 때마다 새로 만들어짐.
  • 클래스 변수 : static이 붙은 변수 예) count

    • 클래스 변수, 정적 변수, static 변수라고 불림. 3개 다 사용
    • static이 붙은 변수는 클래스에 바로 접근 가능, 클래스 자체에 소속
    • 클래스 변수는 프로그램 시작 시 딱 1개 만들어짐 공유하는 목적으로 사용

변수와 생명주기

  • 지역변수(매개변수 포함) : 지역 변수 - 스택 영역에 있는 스택 프레임에 저장
    • 메서드가 종료되면 함께 제거, 그래서 생존 주기가 짧음.
  • 인스턴스 변수 : 인스턴스에 있는 멤버 변수를 말함 - 힙 영역
    • GC가 발생하기 전까지는 생존하기 때문에 보통 지역 변수보다 생존 주기가 길음.
  • 클래스 변수 : 메서드 영역의 static 영역에 보관
    • 메서드 영역은 프로그램 전체에서 사용되는 공용 공간이다. jvm이 종료될때까지 생명주기가 이어짐. 따라서 제일 김.

static이 정적인 이유?

인스턴스 변수는 동적으로 생성되고 제거된다. 하지만 정적변수는 프로그램 실행 시점에 딱 만들어지고, 프로그램 종료 시점에 제거된다.

정적 변수 접근 방법

  • 인스턴스를 통해 접근
    • 추천 x, 코드를 읽을 때 인스턴스 변수에 접근하는 것처럼 오해(인텔리제이에서도 노란줄 뜸)
  • 클래스를 통해 접근
    • 정적 변수는 클래스에 공용으로 관리, 따라서 클래스를 통해 접근하는 것이 명확.

static 메서드

  • 정적 메소드는 정적 변수처럼 인스턴스 생성 없이 클래스 명을 통해서 바로 호출 가능
  • 유틸리티 관련 메서드를 만드는데 유용하게 사용된다.

특정 문자열을 꾸며주는 간단한 기능을 만들어보자.
예를 들어서 "hello" 라는 문자열 앞 뒤에 을 붙여서 "hello*" 와 같이 꾸며주는 기능이다


  • 인스턴스 메서드
package static2;
public class DecoUtil1 {
 public String deco(String str) {
 String result = "*" + str + "*";
 return result;
 }
}
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() 메서드를 호출하기 위해서는 DecoUtil1의 인스턴스를 먼저 생성해야 한다. 그런데 deco()라는 기능은 멤버 변수도 없고, 단순히 기능만 제공.

  • 인스턴스가 필요한 이유는 멤버 변수등을 사용하는 목적이 큰데, 이 메서드는 사용하는 인스턴스 변수도 없고 단순히 기능만 제공

  • static 메서드 활용
package static2;
public class DecoUtil2 {
 public static String deco(String str) {
 String result = "*" + str + "*";
 return result;
 }
}
  • Static 붙임
package static2;
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이 붙지 않은 메서드는 인스턴스를 생성해야 호출할 수 있다.

statoc 메서드 사용법

  • static 메서드는 static만 사용할 수 있다.
    • static 메서드는 static만 사용가능
  • 반대로 모든 곳에서 static을 호출할 수 있다.
    • 정적 메서드는 공용기능

DecoData

package static2;
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);
 }
}
  • instanceValue 는 인스턴스 변수이다.
  • staticValue 는 정적 변수(클래스 변수)이다.
  • instanceMethod() 는 인스턴스 메서드이다.
  • staticMethod() 는 정적 메서드(클래스 메서드)이다

DecoDataMain

package static2;
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();
 }
}

- 정적 메서드가 인스턴스의 기능을 사용할 수 없는 이유

정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있다. 그래서 인스턴스처럼 참조값의 개념이 없다. 특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데, 정적 메서드는 참조값 없이 호출한다. 따라서 정적 메서드 내부에서 사용 불가능

static 메서드 용어 정리

멤버 메서드의 종류(멤버 메서드: 클래스에 정의한 메서드)

  • 인스턴스 메서드: static이 붙지 않은 멤버 메서드
  • 클래스 메서드: static이 붙은 멤버 메서드
    • 정적, static 메서드등으로 부름

static이 붙지 않은 멤버 메서드 = 인스턴스를 생성해야 생성 가능하고, 인스턴스에 소속
static이 붙은 멤버 메서드 = 클래스에 바로 접근해서 사용 가능하고, 클래스에 소속


정적 메서드 활용

객체 생성이 필요 없이 메서드의 호출만으로 필요한 기능을 수행 할 때 주로 사용

정적 메서드 접근법

  • 인스턴스를 통한 접근 data3.staticCall()
    정적 메서드의 경우 인스턴스를 통한 접근은 추천하지 않는다. 왜냐하면 인스턴스 메서드에 접근하는 것처럼 오해할 수 있기 때문
  • 클래스를 통한 접근 DecoData.staticCall()
    정적 메서드는 클래스에서 공용으로 관리하기 때문에 클래스를 통해서 접근하는 것이 더 명확

static import


이렇게 클래스명을 계속 써야 하는 경우가 있다.

  • 특정 클래스의 정적 메서드 하나만 적용하려면 다음과 같이 생략할 메서드 명을 적어주면 된다.
    = import static static2.DecoData.staticCall;
  • 특정 클래스의 모든 정적 메서드에 적용하려면 다음과 같이 을 사용하면 된다.
    = import static static2.DecoData.
    ;

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

main() 메서드와 static 메서드 호출 예)

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

  • 이렇게 static이 빠지면 인스턴스를 생성해야 호출할 수 있으므로 써줘야 한다.

문제풀이

  • car 클래스를 작성하자.

답:

package static2.ex;

public class Car {

    private static int totalCars;// 공용
    private String name;//인스턴스마다 다르므로 static이 안붙음


    public Car(String name) {
        System.out.println("차량 구입, 이름: " + name);
        this.name = name;
        totalCars++;
    }

    public static void showTotalCars() {
        System.out.println("구매한 차량 수= " + totalCars);
    }
}

이 문제는 이게 중요하다.


2번 문제

정답

package static2.ex;

import static static2.ex.MathArrayUtils.*;

public class MathArrayUtilsMain {
 public static void main(String[] args) {
     int[] values = {1, 2, 3, 4, 5};
     System.out.println("sum=" + sum(values));
     System.out.println("average=" + average(values));
     System.out.println("min=" + min(values));
     System.out.println("max=" + max(values));
     }
}
  • import를 사용해서 코드가 깔끔해지고 정적메서드 적용
package static2.ex;

public class MathArrayUtils {

    private MathArrayUtils(){
        //private 인스턴스 생성을 막는다.
    }

    public static int sum(int[] values) {
        int total = 0;
        for (int value : values) {
            total += value;
        }
        return total;
    }

    public static double average(int[] values) {
        return (double) sum(values) / values.length;
    }

    public static int min(int[] values) {
        int minValue = values[0];
        for (int value : values) {
            if (value < minValue) {
                minValue = value;
            }

        }
        return minValue;
    }

    public static int max(int[] values) {
        int maxValue = values[0];
        for (int value : values) {
            if (value >maxValue) {
                maxValue = value;
            }
        }
        return maxValue;
    }
}

정리

  • private 클래스명() {}을 통해 인스턴스 생성을 막았다. 인스턴스 생성은 메모리만 잡아먹는다. 멤버변수도 없고 static만 있기 때문에 제약을 건다.
  • alt + enter로 메서드 만들고 채워넣기 이중 for문으로 루프를 돌려서
    • 더하기는 차례대로 더하게끔
    • 평균은 더한거에서 길이를 나누고
    • 최솟값은 배열 첫번째부터 하나씩 비교해서 나오게끔 함
    • 최대값은 배열 첫번째부터 하나씩 비교해서 나오게끔 함

0개의 댓글