프로그램의 메모리 구조

짱J·2023년 3월 23일
0
post-thumbnail

메모리 사용 방식

기계어를 포함한 모든 프로그래밍 언어의 메모리 사용 방식은 공통적으로 코드 실행 영역데이터 저장 영역으로 나뉜다.

그 중, 객체 지향 프로그램은 데이터 저장 영역을 static, stack, heap 세 가지 영역으로 나누어 사용한다.

이번 포스트에서는 데이터 저장 영역에 집중하여 알아보고자 한다.
삼분할된 데이터 저장 영역이 알파벳 T를 닮았다고 하여 책에서는 T 메모리 구조라고 지칭한다.

그리고 static, stack, heap 영역을 각각 클래스, 메서드, 객체들의 놀이터라고 비유한다.

1️⃣ main() 메서드

main() 메서드는 프로그램이 실행되는 시작점이다.
main() 메서드가 실행될 때, 메모리에 어떤 일이 일어나는지 아래 예제를 통해 알아보자.

public class Start {
	public static void main(String[] args) {
    	System.out.println("Hello OOP!");
    }
}

JDK, JRE, JVM에 대해 알고 싶다면, 이전에 작성한 [Java] JDK, JRE, JVM를 읽고 오자!

  1. JRE가 프로그램 안에 main() 메서드가 있는지 확인한다.
  2. 존재가 확인되면, JRE는 JVM을 부팅한다.
  3. JVM은 목적 파일을 받고, 실행한다.

전처리 과정

JVM이 가장 먼저 하는 것은 전처리 과정이다.
전처리 과정에서 JVM은 java.lang 패키지, 작성한 클래스와 import 패키지를 static 영역에 넣는다.

  • java.lang 패키지를 통해 System.out.println() 같은 메서드를 사용할 수 있다.

System.out.println("Hello OOP!")

전처리 과정은 마무리되었다.
이제 System.out.println("Hello OOP!") 구문이 실행되는 과정을 알아보자.

  1. 여는 중괄호를 만날 때마다 스택 프레임이 생긴다.
  2. 메서드의 인자 args를 저장할 공간(변수 공간)을 스택 프레임 맨 밑에 확보해야 한다.

출력문이 실행되고, main() 메서드의 끝을 나탄는 닫는 중괄호를 만나면, 스택 프레임이 위와 같이 소멸된다.
main() 메서드가 끝나면 JRE는 JVM을 종료하고, JRE도 운영체제 상의 메모리에서 사라진다.

2️⃣ 변수와 메모리

이번에는 변수를 선언하고, 할당하는 예제를 살펴보자.

public class Start2 {
	public static void main(String[] args) {
    	int i;
        i = 10;
        
        double d = 20.0;
    }
}

3번째 줄은 메모리에 4바이트 크기의 정수 저장 공간을 마련하라는 의미이다.
해당 공간은 main() 메서드 스택 프레임 안에서 밑부터 차곡차곡 마련된다.

3번째 줄까지 실행됐을 때 T 메모리의 상태는 위와 같다.
?에는 알 수 없는 쓰레기 값(이전에 해당 공간의 메모리를 사용했던 다른 프로그램이 청소하지 않고 간 값)을 가지고 있다.

4번째 줄, 6번째 줄을 실행한 후 T 메모리의 상태는 위와 같다.
이 때, 6번째 줄은 하나의 명령문이 아니라 두 개의 명령문이다.
변수를 선언하는 명령문과 값을 할당하는 명령문 2개가 한 줄에 있는 것이다.

7번째 줄의 닫는 중괄호를 만나면 main() 메서드 스택 프레임이 스택 영역에서 사라지며 프로그램이 종료된다.

3️⃣ 블록 구문과 메모리

이번에는 블록 구문을 사용했을 때의 T 메모리 구조에 대해 살펴보자.

public class Start3 {
	public static void main(String[] args) {
    	int i = 10;
        int k = 20;
        
        if(i==10) {
        	int m = k + 5;
            k = m;
        } else {
        	int p = k + 10;
            k = p;
        }
        
	// k = m + p;
    }
}

5번째 줄까지 실행했을 때 T 메모리 구조는 위와 같다.

앞서 main() 메서드 예제를 보며, 여는 괄호를 만나면 스택 프레임이 생성된다고 하였다.
6번째 줄에서 if문의 조건을 판단하여, 참인 if 블록 스택 프레임이 만들어진다.
그리고 7번째 줄을 만나 변수 m이 if 블록 스택 프레임 안에 저장된다.

8번째 줄이 실행되면, k의 값이 20에서 25로 변경된다.

9번째 줄에서 if 블록 중 참일 때의 블록을 닫는 중괄호를 만나면, if 블록 스택 프레임은 스택 영역에서 사라진다.

if문 조건의 결과가 true이기 때문에, else 문 블록은 스택 메모리에 등장조차 하지 않는다.

14번째 줄 주석을 해제하면 어떤 일이 일어날까?🤔

예제 코드에서 14번째 줄의 주석을 지워 코드가 아래와 같다고 해보자.

public class Start3 {
	public static void main(String[] args) {
    	int i = 10;
        int k = 20;
        
        if(i==10) {
        	int m = k + 5;
            k = m;
        } else {
        	int p = k + 10;
            k = p;
        }
        
	k = m + p;
    }
}

변수 m과 변수 p를 더하여 변수 k에 대입하는 구문이다.
그러나, T 메모리에는 변수 m과 p가 존재하지 않기 때문에 컴파일러 경고 메시지가 뜨며 에러가 발생한다.

3️⃣ 지역 변수와 메모리

변수는 메모리 상에 존재한다.
그러면 static 영역, stack 영역, heap 영역 셋 중 어느 영역에 있을까?
정답은 '세 군데 모두' 이다.
그러나 세 영역에 있는 변수는 각각 지역 변수, 클래스 멤버 변수, 객체 멤버 변수로 각기 다른 목적을 가지고 있다.

  • 지역 변수 - stack 영역에 존재하며, 스택 영역이 사라지면 함께 사라진다.
  • 클래스 멤버 변수 - static 영역에 존재하며, JVM이 종료될 때까지 유지된다.
  • 객체 멤버 변수 - heap 영역에 존재하며, 가비지 컬렉터에 의해 사라진다.

이 중 지역 변수에 대한 예제를 보며 지역 변수에 대해 자세히 알아보자.

public class Start3 {
	public static void main(String[] args) {
    	int i = 10;
        int k = 20;
        
        System.out.println(m);
        
        if(i==10) {
        	int m = k + 5;
            k = m;
        } else {
        	int p = k + 10;
            k = p;
        }
        
	// k = m + p;
    }
}

if 문 앞에 다음과 같이 System.out.println문이 추가되었다고 해보자.
14번째 줄 주석을 해제하면 어떤 일이 일어날까?🤔에서 본 것과 같이 메모리 상에 존재하지 않는 변수를 참조하려고 하여 컴파일 오류가 발생한다.

public class Start3 {
	public static void main(String[] args) {
    	int i = 10;
        int k = 20;
        
        if(i==10) {
        	int m = k + 5;
            k = m;
        } else {
        	int p = k + 10;
            k = p;
        }
        System.out.println(m);
	// k = m + p;
    }
}

12번째 줄이 끝나고 나서도 변수 m은 존재하지 않으므로 컴파일 오류가 발생한다.

7번째 줄 int m = k + 5에서는 if 블록 스택 프레임 외부에 있는 변수 k에 접근이 가능했지만, if 블록 스택 프레임 외부에서는 내부에 있는 변수인 m에 접근하는 것이 불가능했다.

이를 통해 우리는 한 가지 규칙을 찾아낼 수 있다.

외부 스택 프레임에서 내부 스택 프레임의 변수에 접근하는 것은 불가능하나 그 역은 가능하다.

4️⃣ 메서드 호출과 메모리

이번에는 메서드 호출 과정에서의 T 메모리 변화에 대해 살펴보자.

public class Start4 {
	public static void main(String[] args) {
    	int k = 5;
        int m;
        
        m = square(k);
    }
    
    private static int square(int k) {
    	int result;
        
        k = 25;
        
        result = k;
        
        return result;
    }
}

5번째 줄까지 실행됐을 때 T 메모리 구조는 다음과 같다.
그 다음 6번째 줄에서 square() 메서드가 호출되며, 메서드가 선언된 9번째 줄로 이동한다.

  1. 6번째 줄에서 우리는 square() 메서드의 인자로 k(=5)를 받았다. 그러므로 11번째 줄까지 실행한 뒤 square() 스택 프레임의 변수 k 공간에 5가 할당된다.
  2. 12번째 줄이 실행되며 k=25를 만나 k가 25로 바뀐다.
  • 이 때, main() 메서드의 k와 square() 메서드의 k는 이름만 같지, 서로 다른 변수 공간에 저장된 것을 주목해야 한다. → 이런 것을 Call By Value라고 한다
  • 즉, sqaure() 메서드 안에서 k가 어떻게 변하든 main() 메서드 안의 k에는 영향이 없다!
  1. 14번째 줄이 실행되며 result 변수에 25라는 값이 할당된다.
  2. 16번째 줄이 실행되며 반환값 변수에 result 변수의 값이 복사된다.

main()에서 square() 메서드 내부의 지역 변수에 직접 접근하거나, square()에서 main() 메서드 내부의 지역 변수에 직접 접근하는 것은 💥불가능💥하다.

5️⃣ 전역 변수와 메모리

두 메서드 사이에 값을 전달하는 방법은 세 가지가 있다.
1. 메서드를 호출할 때 인자를 이용
2. 메서드를 종료할 때 반환값으로 넘겨줌
3. 전역 변수를 사용

public class Start5 {
	static int share;
    
    public static void main(String[] args) {
    	share = 55;
        int k = func(5, 7);
        
        System.out.println(share);
    }
    
    private static int func(int m, int p) {
    	share = m + p;
        
        return m - p;
    }
}

static 키워드가 붙은 전역 변수 share은 static 영역에 변수 공간이 할당된다.
5번째 줄까지 실행되면, main() 스택 프레임이 만들어지고, 55가 share에 할당된다.

7번째 줄이 실행되며, 12번째 줄로 넘어가 func() 메서드 스택 프레임이 생성된다.
인자값과 반환값을 저장할 변수 공간도 생기고, 값이 할당된다.

15번째 줄까지 실행되면 변수 share의 값이 바뀌고,반환값에 -2가 저장된다.

16번째 줄의 닫는 괄호를 만나며 func() 메서드 스택 프레임이 사라지고, 7번째 줄로 돌아가 k에 값이 할당된다.

전역 변수는 어느 곳에서나 접근할 수 있으며, 여러 메서드들이 공유해서 사용하기 때문에 공유 변수라고도 한다.

💥 하지만 전역 변수는 사용을 자제해야 한다. 💥
코드가 길어지면서 여러 메서드들이 전역 변수의 값을 변경하기 시작하면, 변수에 저장되어 있는 값을 파악하기 쉽지 않기 대문이다.
그러나, 읽기 전용으로 값을 공유하는 전역 변수를 사용하는 것은 적극 추천된다.

6️⃣ 멀티 스레드와 멀티 프로세스

  • 멀티 스레드는 스택 영역을 스레드의 개수만큼 분할하여 사용한다.
    • 하나의 스레드에서 다른 스레드의 stack 영역에는 접근하지 못하지만, static과 heap 영역은 공유하여 사용하므로 메모리 사용량이 적다.
  • 멀티 프로세스는 다수의 T 메모리를 갖는다.
    • 하나의 프로세스가 다른 프로세스의 T 메모리에 접근하지 못해 안전하지만, 메모리 사용량이 크다.

쓰기가 가능한 전역 변수를 사용하면, 스레드의 안전성이 깨지기 때문에 사용을 지양하는 것이다.
이를 보완하는 방법으로 lock을 거는 방법도 있지만 그렇게 한다면 멀티 스레드의 장점을 활용하지 못하는 것이다.

☘️ Reference

  • 도서 '스프링 입문을 위한 자바 객체 지향의 원리와 이해'
profile
[~2023.04] 블로그 이전했습니다 ㅎㅎ https://leeeeeyeon-dev.tistory.com/

0개의 댓글