자바의 메모리 구조에 대해 알아보기 전에, 우선 JVM에 대해서 이야기 할 필요가 있다.
자바의 가장 큰 특징 중 하나가 바로 플랫폼에 독립적이라는 것인데,
이것은 자바로 작성된 프로그램은 어떠한 운영체제에서도 실행시킬 수 있다는 의미이다.
리눅스 환경에서 실행하던 C 프로그램이 있는데, 이를 윈도우 환경에서도 실행시키려고 하면 어떻게 해야 할까?
리눅스와 윈도우는 완전히 별개의 운영체제이기 때문에, 우선 프로그램의 소스파일을 윈도우 환경에 맞게 다시 컴파일 및 빌드해 주어야 한다.
즉, 프로그램이 실행되는 플랫폼(운영체제)가 바뀔 때마다 기존 소스파일을 새로 컴파일 해 주고 새로 빌드해야 한다.
리눅스 환경에서 실행중인 자바 프로그램을 윈도우 환경에서 실행시키기 위해 해야 할 일은 딱 하나다.
바로 알맞은 JVM을 윈도우 환경에 설치하는 것이다.
그 후에는 우리가 리눅스 환경에서 사용하던 자바 프로그램의 실행파일을 그대로 윈도우에 옮겨 오기만 하면 된다.
즉, 자바 소스파일을 새로 컴파일하고 빌드하는 과정이 필요 없어지게 되는 것이다.
이것이 가능한 이유는 자바 프로그램은 JVM이라는 가상머신 위에서 실행되기 때문이다.
우리가 작성하는 자바 프로그램의 소스파일은 .java 형식으로 저장되고, 이 소스파일을 컴파일 하게 되면 .class라는 바이트코드 파일로 변환된다.
JVM은 이 바이트코드를 읽어들이고, JVM이 설치되어 있는 운영체제가 이해할 수 있는 기계어로 바이트코드를 번역한다.
자바 소스코드를 곧바로 기계어로 번역하지 않고 바이트코드라는 중간 단계 언어로 번역하기 때문에,
개발자는 바이트 코드만 가지고 JVM이 설치되어 있는 운영체제 어디에서라도 자바 프로그램을 실행시킬 수 있게 되는 것이다. (.java -> .class 컴파일 불필요.)
한마디로 리눅스에서 실행하던 자바 프로그램을 USB에 담아 윈도우에서 실행시키는 것이 가능하다는 이야기이다.
우리가 자바 프로그램을 작성하고, 실행하는 흐름을 따라 위 다이어그램의 요소들을 살펴보도록 하자.
1. 우리가 가장 처음 자바 프로그램을 작성하면, 해당 소스는 .java 파일로 저장된다.
그리고 이 소스파일을 javac이라는 컴파일러를 통해 .class 확장자를 갖는 바이트코드 파일로 전환시킨다.
2. 이후, 컴파일이 완료된 자바 프로그램을 실행시키면 JVM은 운영체제로부터 프로그램 실행을 위한 메모리를 할당받는다.
그중 우리가 작성한 프로그램이 동작하는 메모리 영역을 Java Runtime Data라고 부른다.
3. JVM에 존재하는 Class Loader가 .class 파일에서 바이트 코드를 읽어 메모리에 올린다.
4. Execution Engine은 Class Loader를 통해 메모리에 올라온 바이트코드를 명령어 단위로 하나씩 실행시킨다.
Execution Engine은 기본적으로 인터프리팅 방식으로 바이트 코드를 실행한다.
즉, 바이트 코드를 한줄씩 기계어로 번역하면서 프로그램을 실행시킨다.
이때 실질적으로 바이트코드를 기계어로 번역하는게 JIT 컴파일러와 인터프리터이다.
기본적으로 자바는 바이트코드를 한줄 한줄 인터프리터를 이용해 기계어로 번역한다.
다만, 이 경우 성능이 떨어질 수 밖에 없기 때문에, JIT 컴파일러는 인터프리터가 읽어온 바이트코드를 캐싱해 뒀다가 적절한 시점에 기계어로 번역한다.
이후에는 동일한 코드에 대해 인터프리팅을 진행하지 않고, 곧장 번역해 놓은 기계어를 실행한다.
Execution Engine에는 Garbage Collector(GC)가 존재한다. 자바는 C/C++과는 달리 메모리 관리를 자동으로 해 주는데, 더이상 사용되지 않는 메모리 영역을 탐지하고, 비워주는 일을 이 Garbage Collector(GC)가 해 준다. 자세한 내용은 다른 포스트에서 다루도록 하겠다.
5. 자바 프로그램을 작성할 때 사용하는 라이브러리 중에는 C/C++과 같이 다른 언어로 작성된 라이브러리가 존재할 수 있는데 그러한 라이브러리를 자바 환경에서 사용할 수 있게 해 주는 것이 Java Native Interface(JNI)이다.
자바의 메모리 영역은 크게 5개 영역으로 나누어 볼 수 있다.
1. Method 영역
2. Heap 영역
3. Stack 영역
4. PC register 영역
5. Native Method stack 영역
이 중, Method 영역과 Heap 영역은 하나의 자바 프로세스에 존재하는 모든 쓰레드간 공유되는 영역이다.
JVM이 실행되어 클래스가 로딩될 때 생성되며 다음과 같은 데이터들이 저장된다.
타입 정보 : 클래스명, 접근 제어자
필드 정보 : 클래스 내 필드 접근 제어자, 필드 타입, 그 외 제어자(static, final 등)
메서드 정보 : 메서드 이름, 리턴 타입, 파라미터 리스트 및 각 파라미터의 타입 등
Class variable : static으로 선언된 변수가 저장된다.
Runtime Constant Pool : 클래스, 필드, 메서드로의 모든 레퍼런스 데이터를 저장한다.
JVM은 이 데이터를 이용해 실제 메모리상의 위치를 찾아서 필드나 메서드에 접근한다.
일종의 메타데이터들을 담고 있는 영역으로, 모든 쓰레드에서 공유하고 있는 공간이다.
프로그램이 실행되면서 동적으로 생성되는 데이터들이 저장되는 공간이다.
참조형 데이터 타입을 갖는 데이터(객체의 인스턴스, 배열의 공간)들이 이 영역에 할당된다.
C/C++에서는 이 힙 영역에 할당된 공간을 개발자가 직접 해제해 줘야 했지만, 자바에서는 Garbage Collector(GC)가 자동으로 불필요한 공간을 탐지하고 비워준다.
Heap 영역은 모든 쓰레드들이 공유할 수 있는 메모리 공간이다. 즉, 쓰레드간의 동기화 문제가 발생할 수 있다.
메서드 호출시 받아온 매개변수, 메서드 실행중 생성하는 지역변수, 리턴값 등이 저장되는 공간이다.
하나의 메서드가 실행될 때마다 해당 메서드가 실행될 때 필요한 스택 메모리 공간을 묶어서 스택 프레임이라고 부른다.
메서드가 실행될 때마다 이 스택 프레임 단위로 스택 메모리를 할당하고, 메서드 실행이 끝나면 스택 프레임 단위로 메모리를 해제한다.
메서드 지역변수로 참조형 데이터 타입(객체 혹은 배열)이 필요한 경우, 실질적인 인스턴스를 위한 메모리 공간은 Heap 영역에 생성하게 되고, 스택 메모리에는 해당 영역의 주소값을 저장하게 된다.
C/C++에서 포인터 변수에 메모리 주소값이 들어갔던 것을 생각하면 이해하기 편하다.
쓰레드마다 독립적인 메서드 수행을 보장하기 위해, Stack 영역은 쓰레드마다 개별적으로 할당된다.
PC는 Program Counter의 약자로, 현재 쓰레드가 현재 실행하고 있는 코드의 주소값을 가지고 있는 영역이다.
자바 코드가 실행되다보면, 자바 코드가 아닌 다른 코드(C/C++ 등으로 작성된 라이브러리)를 실행하는 경우가 발생하는데, 이때의 PC register에 저장되는 값은 Undefined이다.
Stack과 마찬가지로, 쓰레드마다 독립적인 메서드 수행을 보장하기 위해, Stack 영역은 쓰레드마다 개별적으로 할당된다.
자바 이외의 언어로 작성된 코드가 실행되기 위해 필요한 스택 영역이다.
JNI를 통해 다른 언어로 작성된 메서드가 실행되는 경우, 해당 메서드의 스택프레임이 그냥 Stack 영역이 아닌 Native Method Stack 영역에 쌓이게 된다.
Stack, PC Register와 마찬가지로, 쓰레드마다 독립적인 메서드 수행을 보장하기 위해, Stack 영역은 쓰레드마다 개별적으로 할당된다.
JIT 컴파일러 - https://www.ibm.com/docs/ko/sdk-java-technology/8?topic=reference-jit-compiler
Java Virtual Machine - https://www.realjavaonline.com/java-virtual-machine/java-virtual-machine.php