JAVA의 가장 기본이 되는 내용

자바 메모리 구조 ✨

cwchoiit 2024. 3. 27. 08:34
728x90
반응형
SMALL

참고 자료:

 

김영한의 실전 자바 - 중급 1편 | 김영한 - 인프런

김영한 | 실무에 필요한 자바의 다양한 중급 기능을 예제 코드로 깊이있게 학습합니다., [사진]국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바

www.inflearn.com

 

메서드 영역

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

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

스택 영역

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

  • 스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이다. 메서드를 호출할 때 마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거된다.
참고: 스택 영역은 더 정확히는 각 쓰레드 별로 하나의 실행 스택이 생성된다. 따라서 쓰레드 수 만큼 스택 영역이 생성된다. 쓰레드를 한 개만 사용하면 스택 영역도 하나이다. 쓰레드가 2개면 스택 영역도 두개이다.

힙 영역

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

 

 

메서드 코드는 메서드 영역에 존재한다.

자바에서 특정 클래스로 100개의 인스턴스를 생성하면, 힙 메모리에 100개의 인스턴스가 생긴다. 각각의 인스턴스는 내부에 변수와 메서드를 가진다. 같은 클래스로부터 생성된 객체라도, 인스턴스 내부의 변수 값은 서로 다를 수 있지만 메서드는 공통된 코드를 공유한다. 따라서 객체가 생성될 때, 인스턴스 변수에는 메모리가 할당되지만 메서드에 대한 새로운 메모리 할당은 없다. 메서드는 메서드 영역에서 공통으로 관리되고 실행된다. 정리하면 인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행한다.

 

스택과 큐

스택(Stack)

스택영역을 자바에서 가지고 있으면 스택이 어떤 구조로 이루어졌는지 이해해야 한다. 스택(Stack)은 쉽게 생각해서 위에만 구멍이 있는 통이라고 생각하면 된다.

 

스택에 블럭을 넣는다.

 

스택에 블럭을 뺀다.

 

어떤 구조인가? 가장 마지막에 넣은것이 가장 먼저 나오게 되는 후입선출(LIFO: Last In First Out) 구조를 가지고 있다.

 

큐(Queue)

큐는 위와 아래에 모두 구멍이 있는 통이라고 생각하면 된다.

 

어떤 구조인가? 가장 먼저 넣은 블럭이 가장 먼저 빠져나오는 선입선출(First In First Out) 구조이다. 이것을 큐라고 한다.

왜 이런 자료 구조를 알아야 하냐면 각자 필요한 영역이 있기 때문이다. 예를 들어 선착순 이벤트를 진행하는 프로그램을 만들 때 고객이 대기해야 한다면 큐 자료 구조를 사용해야 한다. 먼저 온 손님이 먼저 실행되어야 하니까. 그럼 프로그램 실행과 메서드 호출에는 스택 구조가 적합하다. 생각해보면 A라는 메서드가 있고 그 A라는 메서드는 내부에서 B라는 메서드를 호출한 결과를 받아 어떤 결과를 내는 로직이 있을 때 A를 호출하면 A가 B를 호출한다. 그럼 호출 순서는 A -> B 인데 뭐가 먼저 실행되어야 하나? B다. B가 실행되어야 A가 그 결과를 가지고 자기의 결과를 낼 수 있으니까. 그러니까 프로그램 실행 및 메서드 호출은 스택 구조가 적합하다. 그래서 자료구조에 대해서 이해하고 있는것은 중요하다. 적어도 스택이랑 큐 정도는.

 

위 내용을 코드로 예로 들어보자. 

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

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

    public 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

 

명백하게 스택 구조를 이루고 있다는 게 보인다. 이 코드가 실행되는 동안 스택 영역은 어떤 변화가 일어날까?

  • 처음 자바 프로그램을 실행하면 main()을 실행한다. 이 때 main()을 위한 스택 프레임이 하나 생성된다.
  • main()method1()을 호출한다. method1() 스택 프레임이 생성된다.
    • method1()m1, cal 지역변수(매개변수 포함)를 가지므로 해당 지역 변수들이 스택 프레임에 포함된다.
  • method1()method2()를 호출한다. method2() 스택 프레임이 생성된다. method2()m2 지역변수(매개변수 포함)를 가지므로 해당 지역 변수가 스택 프레임에 포함된다.

 

  • method2()가 종료된다. 이때 method2() 스택 프레임이 제거되고, 매개변수 m2도 제거된다. method2() 스택 프레임이 제거되었으므로 프로그램은 method1()로 돌아간다. 물론 method1()을 처음부터 시작하는 것이 아니라 method1()에서 method2()를 호출한 지점으로 돌아간다.
  • method1()이 종료된다. 이때 method1() 스택 프레임이 제거되고 지역변수(매개변수 포함) m1, cal도 제거된다. 프로그램은 main()으로 돌아간다.
  • main()이 종료된다. 더 이상 호출할 메서드가 없고 스택 프레임도 완전히 비워졌다. 자바는 프로그램을 정리하고 종료한다.

 

스택 영역과 힙 영역이 같이 사용되는 경우

힙 영역에는 객체(인스턴스)가 사용된다고 했다. 그 점을 기억하면서 다음 코드를 보자.

Data

public class Data {
    private int value;

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

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

DataMain

public class DataMain {
    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 data = new Data(10);
        method2(data);
        System.out.println("method1 end");
    }

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

 

 

실행 결과

main start
method1 start
method2 start
data.value = 10
method2 end
method1 end
main end

 

 

실행 결과를 보면 스택 구조로 가장 마지막에 실행된 메서드가 가장 먼저 끝나는 구조로 진행됐음을 알 수 있다.

그럼 그림을 통해 좀 더 깊이 이해해보자.

 

  • 처음 main() 메서드를 실행한다. main() 스택 프레임이 생성된다.

  • main()에서 method1()을 실행한다. method1() 스택 프레임이 생성된다.
  • method1()은 지역변수로 Data data1을 가지고 있다. 이 지역변수도 스택 프레임에 포함된다.
  • method1()new Data(10)을 사용해서 힙 영역에 Data 인스턴스를 생성한다. 그리고 참조값을 data1에 보관한다.

  • method1()method2()를 호출하면서 Data data2 매개변수에 x001 참조값을 넘긴다.
  • 이제 method1()에 있는 data1method2()에 있는 data2 지역변수(매개변수 포함)는 둘 다 같은 x001 인스턴스를 참조한다.

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

  • method1()이 종료된다. method1()의 스택 프레임이 제거되면서 지역 변수 data1도 함께 제거된다.

  • method1()이 종료된 직후의 상태를 보자. method1()의 스택 프레임이 제거되고 지역변수 data1도 함께 제거되었다.
  • 이제 x001 참조값을 가진 Data 인스턴스를 참조하는 곳이 더는 없다. 참조하는 곳이 없으므로 사용되는 곳도 없다. 결과적으로 프로그램에서는 더는 사용하지 않는 객체인 것이다. 이런 객체는 메모리만 차지하게 된다. GC는 이렇게 참조가 모두 사라진 인스턴스를 찾아서 메모리에서 제거한다. (참고로, 힙 영역 외부가 아닌 힙 영역 안에서만 인스턴스끼리 서로 참조하는 경우에도 GC의 대상이 된다.)
정리: 여기서 알아낸 사실은 지역변수는 스택 영역에서 관리되고 객체(인스턴스), 인스턴스 변수는 힙 영역에서 관리되는 것을 확인했다. 이제 나머지 하나가 남았는데 메서드 영역이다. 메서드 영역에서 관리하는 변수도 있다. static 변수가 그렇다. 

 

static 변수

static 변수는 다른 말로 정적 변수, 클래스 변수라고 표현한다. 이 변수를 사용하는 목적은 특정 인스턴스가 가지는 값이 아니라 클래스가 공통으로 사용하는 변수나 메서드를 작성하려고 할 때 사용한다. 그니까 인스턴스마다 달라지는 값이 아니라 모든 인스턴스가 다 동일한 값을 가진다는 소리다. 왜 필요할까? 특정 클래스가 공유하는 값을 전역으로 관리하고 싶은 경우가 있을 수 있기 때문이다. 

 

그리고 인스턴스마다 관리하는 변수가 아니란 소리는 이 static 변수는 힙 영역에서 관리하지 않는다는 말로 유추할 수 있다. 힙 영역은 객체(인스턴스)와 그 객체가 가지는 인스턴스 변수를 관리하는 곳이니까. 그래서 static 변수는 프로그램 전역에서 공통으로 사용되는 것들을 관리하는 메서드 영역에서 관리된다.

 

 

그래서 만약 Data3 이라는 클래스가 있고 그 클래스안에 클래스 변수 count가 있으면 인스턴스를 생성하면 해당 인스턴스와 인스턴스 변수는 힙 영역에서 관리되고 클래스 변수는 메서드 영역 내 static 영역에서 딱 1개만 생성되고 관리된다. 

 

이 상태에서 아무리 Data3 클래스의 인스턴스를 여러개 만들어도 다음과 같이 모두 같은 static 변수를 바라본다.

 

 

변수와 생명주기

  • 지역변수(매개변수 포함): 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관된다. 메서드가 종료되면 스택 프레임도 제거되는데 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거된다. 따라서 지역 변수는 생존 주기가 짧다.
  • 인스턴스 변수: 인스턴스에 있는 멤버 변수 중 static이 붙지 않은 변수를 인스턴스 변수라고 한다. 인스턴스 변수는 힙 영역을 사용한다. 힙 영역은 GC(가비지 컬렉션)가 발생하기 전까지는 생존하기 때문에 보통은 지역변수보다 생명주기가 길다.
  • 클래스 변수: 클래스 변수는 메서드 영역의 static 영역에 보관되는 변수이다. 메서드 영역은 프로그램 전체에서 사용하는 공용 공간이다. 클래스 변수는 해당 클래스가 JVM에 로딩되는 순간  생성된다. 그리고 JVM이 종료될 때까지 생명주기가 이어진다. 따라서 가장 긴 생명주기를 가진다.
static이 정적이라는 이유는 바로 여기에 있다. 힙 영역에 생성되는 인스턴스 변수는 동적으로 생성되고 제거된다. 반면에 static인 정적 변수는 거의 프로그램 실행 시점에 딱 한번 만들어지고 프로그램 종료 시점에 제거된다. 정적 변수는 이름 그대로 정적이다.

 

 

정적 변수 접근법

정적 변수는 클래스로부터 바로 접근도 가능하고 클래스의 인스턴스로도 접근이 가능하다.

Data.count // 가능 (클래스로부터 접근)
data.count // 가능 (인스턴스로부터 접근)

 

근데 인스턴스를 통한 접근은 관례상 추천하지 않는다. 이유가 뭘까? 코드를 읽는 사람은 이것을 보고 인스턴스 변수라고 생각하지 클래스 변수라고 생각하지 않기 때문이다. 관례는 따라야 좋기 때문에 관례이다. 

 

그리고 인스턴스로 접근을 한다고해도 결국 힙 영역에 해당 인스턴스로 가서 count를 찾는데 힙 영역에 없는 메서드 영역에 있는 변수구나!라고 판단해서 결국은 메서드 영역에 static 영역으로 가서 해당 변수에 접근한다.

 

물론, 해당 클래스 변수가 만들어지는 클래스 안에서는 그냥 접근하면 된다. 그냥.

 

 

static 메서드

static 메서드도 있는데 얘는 왜 있을까? 이유를 알아야 그 용도도 올바르게 사용할 수 있다. static 메서드는 해당 클래스의 인스턴스를 만들 필요가 없을 때 static 메서드를 만든다. 아래 코드를 보자.

Utils

public class Utils {

    public String decoration(String str) {
        return "*" + str + "*";
    }
}

UtilsMain

public class UtilsMain {
    public static void main(String[] args) {
        Utils utils = new Utils();
        String str = "Hello";
        String decoration = utils.decoration(str);

        System.out.println(str);
        System.out.println(decoration);
    }
}

 

Utils 클래스에 있는 메서드 decoration()은 문자열을 받아서 해당 문자열 앞뒤로 "*"을 붙여주는 메서드이다. 이것을 사용하는 코드가 UtilsMain이고 여기서 보면 Utils 클래스의 인스턴스를 생성한 후 해당 인스턴스로 decoration()에 접근한다. 근데, 인스턴스를 만들 필요가 있을까? 없다. 인스턴스를 사용하는 부분이 단 한개도 없다. 이럴 때 다음 코드를 아래처럼 변경해보자.

변경한 Utils

public class Utils {
    public static String decoration(String str) {
        return "*" + str + "*";
    }
}

변경한 UtilsMain

public class UtilsMain {
    public static void main(String[] args) {
        String str = "Hello";
        String decoration = Utils.decoration(str);

        System.out.println(str);
        System.out.println(decoration);
    }
}

 

기존 decoration() 메서드를 static 메서드로 변경하고 나니 인스턴스를 만들 필요없이 클래스 레벨로 바로 접근해서 사용했다. 코드 양도 줄고 불필요한 시간 낭비도 줄이고 가독성도 높아진 느낌이다. 이럴 때 static 메서드를 사용한다.

 

그러나, static 메서드는 언제나 사용할 수 있는 것이 아니다. 

정적 메서드 사용법

  • static 메서드는 static만 사용할 수 있다.
    • static 메서드 안에서는 static 변수나 static 메서드만 사용이 가능하고 인스턴스 변수나 인스턴스 메서드는 사용할 수 없다.
  • 반대로 어디서든 static 메서드를 호출할 수는 있다.
    • 접근 제어자만 허락한다면 어디서나 호출할 수 있다. 그러라고 만든 키워드가 static이니까.

아래 그림을 보면 이해가 바로 된다.

힙 영역에 있는 인스턴스 변수를 메서드 영역 내 static 영역에 있는 메서드가 알 길이 있을까? 참조값이 없으면 해당 변수에 접근 자체를 할 수 없다. 모르니까. 인스턴스 메서드는 메서드 영역에 있지만 static 영역에 있는게 아니라 이 또한 마찬가지로 어떤 메서드를 말하는지 참조값을 모르니 알 수 없다. 

728x90
반응형
LIST

'JAVA의 가장 기본이 되는 내용' 카테고리의 다른 글

상속 (Part.1)  (0) 2024.03.28
final  (0) 2024.03.27
캡슐화  (0) 2024.03.26
Class 레벨의 접근제어자  (0) 2024.03.26
Package에서 딱 하나 헷갈리는 한가지  (0) 2024.03.26