Java

Java의 메모리 구조

Recfli 2023. 12. 28. 21:47

[ 작성이유 ]

 Java 쓰레드 프로그래밍에 관한 강의에서도 그리고 "스프링 입문을 위한 자바 객체 지향의 원리와 이해" 책을 읽으면서 Java의 JVM 메모리를 정확하게 잘 모른다는 느낌이 들었다. 그래서 각각의 영역에 해당하는 변수, 메서드들이 왜 동작이 안되는지를 정확하게 모르는 결과로 이어졌다고 생각이 들었고 이를 정리해보고자 한다.

[ 자바의 메모리 ]

 T메모리는 static 영역, stack 영역, heap영역을 합친 영역이다.

 

static 영역

 

 java.lang과 같은 패키지, import된 패키지, 프로그램 상의 모든 클래스가 static 영역에 배치된다. 내가 짠 코드 중 static 영역에 배치되어야 하는 것은 객체가 필요한 코드 실행 시점이 돼야 static에 올라가게 된다.  static 영역에 올라간 것들은 JVM이 종료될 때까지 사라지지 않고 고정된(static) 상태로 유지된다. 이 영역의 코드의 이런 특성 때문에 static 영역의 코드를 짜다보면 생기는 특징이 있다.

 

 "java.lang과 같은 패키지, import된 패키지, 프로그램상의 모든 클래스가 main 메서드를 실행하기 전에 static 영역에 배치된다."는 약간 애매한 말일 수도 있다. - 2024-01-05 수정.

 

첫 번째로는 읽기전용이거나 stateless 한걸 올려주는 게 좋다. state가 유지되는 걸 부득이하게 올리게 되는 경우엔 멀티쓰레드 환경에서는 lock을 걸어주는 게 좋다.

 

 두 번째로는 실행 전에 IDE에서 컴파일이 되는 시점에 알 수 없는 건 미리 올라갈 수 없다고 알려준다. 이 말은 people이라는 class 내에 getInformation이라는 static 메서드가 들어있는 예시를 보면 알 수 있다.

public class people {
    String name;
    static int age;

    static void getInformation(){
        // System.out.println(name);
        System.out.println(age);
    }
}

 

 이 코드를 people.java로 만들어서 작동을 시켜보면 name은 에러가 나서 빨간 불이 들어오고 실행자체를 거부한다. 그 이유는 name은 static 변수값이 아니고 age는 static 변수값이기 때문이다. static 영역에 people이 컴파일되고 main 메서드를 시작하기 전에 올라가면 name과 age라는 변수가 static 영역에 있기는 하지만 static인 age만 메모리와 값이 할당되어있다. 그리고 getInformation는 static 메서드이다. 그렇기 때문에 본인이 static 영역에 올라갈 때 가지고 있는 코드가 그 시점에 알 수 없는거면 안된다. 그렇기 때문에 name은 그 시점에 알 수 없고 static 영역에 다 올리고 main에서 뭔가 객체를 heap영역에 생성해야 알 수 있는 값이기에 안되는 것이다. 반대로 age는 알 수 있으니 되는 것이다.

 

stack 영역

 

 여는 중괄호 '{'마다 스택프레임이 생기고, 닫는 중괄호 '}'마다, if문, 반복문, try문마다 모두 스택프레임이 생긴다. 지역변수라는 걸 잘 생각해보면 스택 프레임 내부에는 임시로 지역변수가 생성될 수 있다. 예시 코드를 한번 봐보자.

public class function {
    static int fun(int a, int b){
        if(a == 5) // 3
            return a + b;
        else
            return a - b;
    }
    public static void main(String[] args){ // 1
        int a = 5;
        int b = 6;
        fun(a,b); // 2
    }
}

 

예시에 관해서 말로 설명을 하면 코드 라인 순서를 설명하기 위해 1,2,3을 붙였다. static 영역에 올라갈 게 올라가고 main이 실행되면, 1이 실행되면, main 스택프레임이 stack영역에 올라온다. 그 스택프레임에는 2전까지 두 줄이 실행되면서 a와 b라는 변수에 메모리와 값이 할당된다. 이제 2가 작동하는 순간 fun이라는 스택프레임이 main과는 별개로 생성이 되고 fun스택프레임에는 a와 b라는 변수에 메모리와 값이 할당된다. 그리고 3이 작동하는 순간에 fun 내부에는 if 스택프레임이 생성이 된다.

 대충 그렸지만 대충 이런 느낌으로 작성이 될 것이다. 그림을 잘보면 fun 함수 스택 프레임 내부에 if 스택 프레임이 있다. 그리고 main과 fun 스택 프레임은 별개이다. 이제 고민 해봐야 될 것은 '서로 다른 스택 프레임을 참조할 수 있는가?'이다. 

 

 정답은 내부 스택 프레임은 외부 스택 프레임을 참조할 수 있지만 외부는 내부를 참조할 수 없고 영역이 분리된 스택 프레임끼리도 참조가 불가하다. 그러니까 if 스택 프레임은 fun 스택 프레임의 값을 참조할 수 있지만 반대는 안된다는 말이고 main스택 프레임과 fun 스택 프레임 간 값도 참조가 불가능하다. a,b는 단지 call by value로 fun의 a,b는 main의 a,b와 다른 새로운 복사된 값이다.

 

추가적으로 쓰레드도 stack영역에 생성되는데 쓰레드 간 stack영역도 침범이 불가능하다.

 

Heap 영역

 

생성된 객체(인스턴스)들이 올라간다. stack영역에는 heap영역에 생성된 객체(인스턴스)를 참조하기 위한 포인터 역할의 참조형 변수를 가지며 이 참조형 변수를 통해서 heap영역에 있는 객체를 수정하고 조회할 수 있다. 또한 상속을 통해 인스턴스를 만들었다면? 부모와 둘이 함께 heap영역에 올라간다. 이 때 알아야할 점은 어떤 객체를 참조형 변수가 가리키고 있는가이다. 아래의 코드를 보고 설명을 읽어보면 된다.

public class Animal {
    public String name;

    public void showName(){
        System.out.printf("동물");
    }
}
public class Dog extends Animal {
    public void barking(){
        System.out.println("왈왈");
    }
}
public class Driver {
    public static void main(String[] args){
        Animal 흰둥이 = new Dog(); // 1
        흰둥이.showName();
        // 흰둥이.barking(); 에러가 난다.
        ((Dog) 흰둥이).barking(); // 2 형변환하면 포인터가 바뀌면서 된다.
    }
}

 

 만약 Animal과 그걸 상속받은 Dog 클래스가 있으면 참조형 변수가 Animal 타입일 시 Dog 래스 내의 메서드는 사용하지 못한다. 설령 'Animal 흰둥이 = new Dog();'로 생성해도 말이다. Animal에는 그런게 없기 때문이다. 쓰고 싶으면 형변환을 해야한다. 단, 오버라이딩된 내용은 변경된 대로 작동한다. 

위의 그림은 각각의 1과 2의 상황에서 흰둥이의 형과 포인터가 가리키는 영역을 표시한 그림이다.

 

그래서 이제 헷갈릴만한 건 클래스가 static에 올라간다는건가? heap에 올라간다는 건가?이다. 고민을 좀 해봤는데 코드 자체에 대한 설정은 static에 올라간다. 메서드, 변수가 올라가는데 static이면 메모리에 값이 할당된 상태로 아니라면 메모리에 값이 할당되지 않은 상태로 올라간다. 그리고 객체(인스턴스)가 새로 만들어지길 요청받는 순간에 static 영역에서 초기화되지 않은 것의 메모리까지 포함해서 heap영역에 새 인스턴스로 올라가게 된다. 이렇게 쯤 생각하면 괜찮지 않을까?

[ 참고자료 ]

https://siyoon210.tistory.com/124 

스프링 입문을 위한 자바 객체지향의 원리와 이해 (김종민 저자)