자바의 T메모리 구조
자바의 RunTime Data Area영역은 스태틱, 스택, 힙 3가지 영역으로 분리하여 간략하게 T 메모리 구조라고 지칭한다. 사실은 조금 더 복잡하지만 변수의 저장 방식에 대해서 설명하기 위해 간략하게 표현한 것이다.
스태틱(Static) 영역 (클래스의 놀이터)
Method Area, Class Area, Code Area라고도 불리며 JVM이 동작해서 클래스가 로딩될 때 생성된다.
스태틱 영역에는 클래스 멤버 변수의 이름, 데이터 타입, 접근 제어자 정보와 같은 각종 필드 정보들과 메서드 정보, 데이터 Type 정보, Constant Pool, static변수, final class 등 클래스와 인터페이스의 구조가 저장되는 공간이다.
해당 메소드와 변수를 선언된 범위 내에서는 어디서든 사용할 수 있으며, 해당 값을 공유가 되며, 할당된 메모리는 프로그램이 종료될 때까지 유지된다.
즉 ‘A’라는 클래스에서 public static Integer cnt = 0;으로 선언됐을 때, ‘B’라는 클래스 안 메서드에서 A의 cnt=1로 바꿔주면 다시 호출됐을 때 cnt 값은 1이 된다.
스택(Stack) 영역(메소드의 놀이터)
스택 영역은 메소드 호출과 관련된 정보를 저장한다. 즉, 메소드 각가의 스택 프레임이 생성되는 영역이다.
지역 변수와 같은 기본 자료형과 객체를 생성했을 때, 객체의 주솟값을 저장하는 영역이다. 이렇게 저장된 변수들은 해당 프레임에서만 사용할 수 있고 외부에서는 사용할 수 없다. 이는 스택 영역에 있는 각 스택 프레임에서도 서로간 접근이 불가능하다는 의미이다.
스택은 후입선출(Last In First Out) 구조로 동작하며 메소드 호출이 끝나면 해당 메소드에서 사용한 스택 영역은 자동으로 사라진다.
※ 참고
[왜 접근이 불가능할까?]
첫 번째 이유는, 메소드는 고유 공간이라 서로 침범할 수 없는게 이치에 맞기 때문이다.
두 번째는 포인터 문제다. 만약 A 스택 프레임에서 B 스택 프레임 내부의 지역 변수에 접근하려면, 해당 지역 변수의 위치를 명확히 알아야하는데, 자바에서는 포인터가 존재하지 않기 때문에 접근이 불가능하다.
세 번째는 메소드는 다양한 곳에서 호출되기 때문이다. 만약 호출한 메소드 내부의 지역 변수를 호출당하는 메소드에서 제어할 수 있게 하려면 포인터를 주고 받아야 하고, 이는 두 번째 문제와 연결된다. 또한 메소드를 호출하면서 만들어지는 스택 구조도 항시 변화할 것이다. 따라서 항시 변하기 때문에 어느 메소드 스택 프레임의 변수를 참조해야하는지에 대한 문제가 발생한다.
힙(Heap)영역(객체의 놀이터)
힙 영역은 동적으로 생성된 객체와 배열이 저장된다. 객체와 배열은 런타임에 생성되기 때문에 크기가 가변적이며 해당 영역은 가비지 컬렉터(GC)에 의해 메모리 관리가 된다.
main() 메서드와 T 메모리
main() 실행 전
메인 메소드가 실행되기 전에는 다음과 같은 과정이 진행된다.
- JRE는 프로그램 안에 main() 메서드가 있는지 확인한다.
- main() 메서드르 확인하면 JRE는 프로그램 실행을 위한 사전 준비를 한다.
- JVM에 전원을 넣어 부팅한다
- 구동된 JVM은 메인 메소드를 실행하기 전, 전처리 과정을 수행한다.
- 즉, JVM은 목적 파일을 받아서 목적 파일을 실행한다.
- JVM은 메인 메소드를 실행한다.
※ 참고
목적 코드(Object code)또는 목적 파일은 컴파일러나 어셈블러가 소스 코드 파일을 컴파일 또는 어셈블해서 생성하는 파일이다. 자바의 경우 .class 파일을 말한다.
JVM 전처리 과정
모든 자바 프로그램이 반드시 포함하는 패키지인 `java.lang` 패키지를 T 메모리의 스태틱 영역에 가져다 놓는다.
JVM은 개발자가 작성한 모든 클래스와 임포트 패키지를 스태틱 영역에 가져다 놓는다.
정리하자면 다음과 같다.
main() 메서드가 실행되기 전 JVM에서 수행하는 전처리 작업들
- java.lang 패키지를 T 메모리의 스태틱 영역에 배치한다.
- imort 된 패키지를 T 메모리의 스태틱 영역에 배치한다.
- 프로그램 상의 모든 클래스를 T 메모리의 스태틱 영역에 배치한다.
더 정확히는 패키지와 클래스는 처음으로 사용될 때 로딩되는 것이 맞다.
스태틱의 별명은 클래스들의 놀이터 라고도 한다.
main() 실행
- main 메소드가 실행되면 스택 프레임이 스택 영역에 할당된다.
- 그리고 스택 프레임 내부에는 main 메소드에 선언된 지역 변수에 대한 공간을 할당해준다. 가장 기본적인 main 메소드라면, main 메소드의 인자 args를 저장할 변수 공간이 스택 프레임의 가장 밑에 확보된다.
- 이 다음에 `System.out.println("Hello OOP");` 구문이 실행된다. 이 때 T 메모리에 변화는 없다.
- main 메모리의 닫는 중괄호를 만나게 되면 스택 프레임은 소멸한다.
이렇게 스택 프레임은 여는 중괄호를 만나면 스택 프레임이 생기며, 닫는 중괄호를 만나면 소멸한다 - main() 메서드가 끝나면 JRE는 JVM을 종료하고 JRE 자체도 운영체제 상의 메모리에서 사라진다. 그러면 T 메모리도 운명을 다하고 사라진다.
변수와 T 메모리
2번째 줄을 실행한 상황은 위와 같다. 이렇게 표현되는 이유는 `main() 메서드와 T 메모리` 에서 설명했다.
- ? 에는 알 수 없는 값이 들어가 있다는 뜻이다.
- 변수 i와 같이 선언만 하고 초기화를 하지 않은 상태에서 출력하려고 하면 자바 컴파일러(javac)는 "The local variable i may not have been initailized" 경고를 출력한다.
6번째 줄을 실행한 후 T 메모리의 상태는 위와 같다.
블록 구분과 메모리: 블록 스택 프레임
- `변수와 T 메모리` 에서 4번 라인까지 실행하면 T 메모리에 어떻게 저장되는지 설명하였다.
- if문은 T 메모리에 어떻게 기록될까? if 는 조건문에 따라 분기가 일어난다. i 에 저장된 값이 10이므로 참인 블록의 스택 프레임이 만들어진다.
- if 블록 중 참일 때의 블록을 종료하는 닫는 중괄호를 만나면 참인 if 블록 스택 프레임은 스택 영역에서 사라진다.
- 또란 if 블록 스택 프레임 안에 상주하던 변수의 저장 공간도 사라진다.
- 만약 라인 20번의 주석을 해제하면 어떻게 될까?
- 에러가 나올 것이다.
- 변수 m은 if 블록 스택 프레임이 종료되면서 사라진다. 그러므로 main이라는 외부 스택 프레임에서 내부 변수 m에는 접근 할 수 없는 것이다.
즉, "외부 스택 프레임에서 내부 스택 프레임의 변수에 접근하는 것은 불가능하나 그 역은 가능하다."
메서드 호출과 메모리: 메서드 스택 프레임
- 6번째 줄에서 square() 메서드를 호출한다. 제어의 흐름이 square() 메서드가 선언된 9번째 줄로 이동할 것이다.
- 위의 그림은 12번째 줄의 실행을 마쳤을 때 T 메모리의 스냅샷이다.
- 주목해야할 것은 main() 메서드가 가진 변수 k와 square() 메서드가 가진 변수 k가 이름은 같지만 실제로는 별도의 공간에 존재한다는 것이다. 이것을 `Call by Value(값에 의한 호출)` 이라고 한다.
- 따라서 square() 메서드 안의 변수에 어떠한 값을 변경하더라도 main() 메서드 안의 k 변수는 영향이 없다.
※ 참고
Call by value?
Call by value는 메소드 호출 방식중 하나로, 전달되는 변수의 값을 복사하여 함수의 인자로 전달하는 것을 의미한다.
- 6번째 줄을 실행한 후 T 메모리 상태이다.
전역 변수와 메모리
- share 변수에 static 키워드가 붙어 있다.
- share 변수는 T 메모리의 스태틱 영역에 변수 공간이 할당된다.
- 3번째 줄을 실행하면 T 메모리에 main() 메서드 스택 프레임이 만들어진다.
- 이렇게 share 변수와 같은 변수를 전역 변수라고 한다.
- 전역 변수는 코드 어느 곳에서나 접근이 가능하다. 여러 메서드들이 공유해서 사용한다고 해서 공유 변수라고도 한다.
※ 참고
가급적이면 전역 변수를 사용하지 않는 것이 좋다?
프로젝트 규모에 따라 코드가 커지면서 여러 메소드에서 전역 변수의 값을 변경하기 시작하면 T 메모리로 추적하지 않는 이상 전역 변수에 저장되어 있는 값을 파악하기 쉽지 않아진다. 즉, 유지 보수 과정에서 계속 추적을 해야하는 불편함이 생긴다.
그리고 동시성 이슈도 존재한다. 서로 다른 메소드가 전역 변수에 접근하게 되면 우리가 의도하지 않은 결과가 나올 수 있기 때문이다.
그러므로 `Math.PI` 와 같은 읽기 전용으로 값을 공유해서 전역 상수로 쓰는 것은 추천한다.
멀티 스레드 / 멀티 프로세스의 T 메모리
멀티 프로세스
- 멀티 프로세스는 다수의 데이터 저장영역, 즉 다수의 T 메모리를 갖는 구조이다.
- 그러므로 각 프로세스마다 각자의 T 메모리가 있고 각자 고유의 공간이므로 서로 참조할 수 없다.
- 멀티 프로세스는 하나의 프로세스가 다른 프로세스의 T 메모리 영역을 절대 침범 할 수 없는 안전한 구조이지만 메모리 사용량은 그 만큼 크다
멀티 스레드
- 멀티 스레드의 T 메모리 모델은 스택 영역을 스레드 개수만큼 분할해서 사용하는 것이다.즉, 멀티 스레드는 하나의 T 메모리만 사용하는데 스택 영역만 분할해서 사용하는 구조이다.
- 멀티 스레드는 하나의 T 메모리 안에서 스택 영역만 분할한 것이기 떄문에 하나의 스레드에서 다른 스레드의 스택 영역에는 접근할 수 없지만 스태틱 영역과 힙 영역은 공유해서 사용하는 구조이다. 따라서 멀티 프로세스 대비 메모리를 적게 사용할 수 있는 구조이다.
스프링에서의 T 메모리 구조
String을 개발하게 되면 @Controller과 @Service, @Configuration, @Component 등을 사용하여 Bean을 등록하게 될것이다. 이때 Spring의 Bean 생성 기본 전략은 싱글톤 Bean으로 생성하여 공유하는 것이다.
이를 T 메모리 관점에서 보면 @Component를 해서 Bean을 생성할 때마다 해당 Bean들은 단 하나의 "공유" 라는 컨셉을 위해서 스태틱 영역에 생성되는 것이라고 생각할 수 있다.
이렇게 스태틱 영역에 생성되는 Bean들에 의해서 Spring이 @Autowired와 같은 방법을 통해 IoC, DI를 할 수 있는 것이다.
참고