지난 글에서는 JVM
의 구성 요소 중 .class
파일을 읽어 메모리의 메서드(Method)
영역에 로드하는 클래스 로더에 대해 알아봤다. 사실 클래스 로더에서 다룬 글에서 이미 오늘 다룰 메모리에 관한 내용이 포함되어 있었는데, 이번 글에서는 JVM
메모리에 대해 보다 자세히 다룰 예정이다.
실행 파일 형태의 프로그램이 실행 중 상태에 있을 때 우리는 이를 프로세스(Process)
라 부른다. 이를 또 메모리 관점에서 설명하면 메인 메모리(RAM)에 프로세스가 올라가있는 상태로도 설명할 수 있다.
[ 그림 1 ] 는 프로세스가 메모리에서 어떻게 관리되는지를 보여준다. 여기서 말하고 싶은 핵심은 프로그램이 실행될 때 메모리에서 해당 프로세스를 관리하기 위해 여러 정보들이 올라가 있다는 점이다.
이제 다시 자바 프로그램의 실행 맥락으로 다시 돌아와보자. 위 내용을 기반으로 자바 프로그램의 실행과 관련해서도 필요한 데이터들이 메모리에 올라간다는 생각해볼 수 있다. 단, 자바 프로그램이 실행되면 JVM
이 OS
로부터 해당 프로그램이 필요로하는 메모리를 할당 받고, JVM
은 할당 받은 메모리를 여러 영역으로 나누어 관리한다. 이 내용을 기반으로, 자바 프로그램이 OS
와의 관계에서 JVM
을 매개로 실행되는 관계를 메모리 관점에서도 이해할 수 있다.
[ 그림 2 ] 의 메모리(Runtime Data Area)
를 보면 5개의 세부 영역으로 나뉘어 있는 것을 볼 수 있다.
- Method
- Heap
- Stack
- PC Register
- Native Method Stack
위 목록 중 [ 그림 2 ] 에서 Method
, Heap
은 모든 스레드에서 공유한다는 점에서 다른 영역과 다른 색으로 표시했다. 지금부터 각 영역에서 어떤 정보들이 저장되고 관리되는지 알아보자.
클래스 로더는 .class
내 바이트 코드를 읽어 JVM
메모리의 Method
영역에 클래스 메타 데이터(아래 목록 참고)를 저장한다.
- FQCN (Full-Qualified Class Name)
- Type 정보 (Interface, Class, 또는 Enum)
- 인스턴스 변수, 메서드, 생성자, 정적 변수
객체 생성을 위한 필요한 정보가 저장되어 있는 곳으로, 프로그램 시작부터 종료될 때 까지 다시 말해 프로그램 생명주기 내 지속적으로 메모리에서 로드되어 있는 영역이다. 또한 앞서 언급한 것과 같이 모든 스레드에서 공유하는 공간이다.
Heap
은 런타임 시 new
를 통해 생성된 객체 또는 배열이 저장되는 영역이다. 클래스 로더 설명 중, 로딩 후 Class
타입의 객체를 생성해 Heap
영역에 저장한다고 했었는데, 해당 내용과 같이 런타임 시 생성된 객체가 저장되는 공간으로 이해하면 된다.
Heap
영역은 다른 글을 통해 다룰 Garbage Collector
와 밀접한 관계가 있다. Garbage Collector
는 참조되지 않은 객체(unreachable object)를 제거해주는 역할을 담당하는데, 앞서 설명한 것과 같이 런타임 시 생성된 객체가 저장되는 영역이 Heap
이므로 Garbage Collector
의 주 활동 영역이 된다.
Stack
부터 아래에 등장하는 각 요소는 모두 스레드 별로 따로 할당된다.
각 스레드 별로 메서드를 호출하면, [ 그림 3 ] 와 같이 Stack
에 Stack Frame
이 쌓이고, 메서드가 종료된 경우 Stack
자료 구조 특성과 같이 위에서 부터 하나씩 Stack Frame
이 제거(pop)된다.
Stack Frame
내에는 Local Variable(지역 변수)
정보와 Operand Stack
, Constant Pool Reference
를 포함하고 있다.
Operand Stack
에는 메서드 내 연산을 위한 명령어가 저장된 작업 공간이고, Constant Pool Reference
는 말 그대로 Constant Pool
참조를 위한 공간이다.
예를 들어, [ 그림 4 ] 의 main()
메서드가 호출되는 경우를 생각해보자. 메서드가 호출되면, [ 그림 5 ] 과 같이 Stack Frame
이 Stack
에 추가되고, Local Variable
에 해당하는 args, var
은 Stack Frame
내에서 저장되어 있다. 이 때 var
변수와 같이 Constant Pool
을 참조하는 경우, 앞서 언급한 것과 같이 Stack Frame
의 Constant Pool Reference
내에서 해당 참조를 관리하게 된다.
자바 프로그램에서 자바가 아닌 언어(C, C++)로 작성된 API 를 사용하는 경우가 있다. 이런 메서드를 네이티브 메서드라고 하고, 해당 메서드가 바이트 코드가 아닌 기계어로 작성되어 있으므로 JVM
이 이를 처리할 때는 기계어를 다시 바이트 코드로 변환되어야 한다.
기계어 ► 바이트 코드
변환 작업을 위해 [ 그림 6 ] 의 Native Method Interface
를 활용한다. Native Method
활용 예로, 자바 코드로 작성된 메서드를 수행하다가 Native Method Interface
를 통해 Native Method
를 호출하는 경우가 있다. 이 때 Native Method Stack
에 Frame이 쌓이고, Stack
과 Native Method Stack
은 동적 연결(Dynamic Linking)을 통해 연결된다.
[ 그림 7 ]
[ 그림 7 ] 은 1개의 CPU
가 시분할 방식으로 프로세스를 처리하는 것을 보여준다.
[ 그림 7 ] 에서 1개의 CPU
가 여러 개의 프로세스를 매우 짧은 시간동안 실행하는 것을 반복(= 시분할 방식)하면서 우리 눈에는 마치 여러 개의 프로세스가 동시에 실행되는 것 처럼 보인다.
이 때 각 프로세스가 대기 상태(CPU 점유 X)에 있다가 실행 상태(CPU 점유 O)로 다시 돌아올 때 이전에 진행 했던 코드(명령어)부터 시작해야 하는데, 이 때 이러한 정보를 PC(Program Counter) 에 저장한다.
자바의 PC Register
역시 위 내용과 같은 맥락을 가지고 있고, 이 곳에 각 스레드 별로 스레드가 실행할 스택 프레임 정보를 가리키는 포인터 정보를 저장한다.
JVM에 관하여 - Part 3, Run-Time Data Area
Where Does Java’s String Constant Pool Live, the Heap or the Stack? | Baeldung
JVM stack과 frame - 기계인간 John Grib
What is Java String Pool? | DigitalOcean Java 런타임 데이터 영역 :: 스터디룸