이전 게시물들에서 자바 코드가 어떻게 메모리에 적재되어 실행되며, 메모리 상에서 어떻게 관리되는지 살펴보았다.
이번 게시물에서는 JVM의 메모리 구조가 어떻게 이루어져 있는지 살펴보자.

런타임 데이터 영역은 JVM의 메모리 영역으로 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역.
모든 쓰레드가 공유하는 영역과 쓰레드 별로 가지고 있는 메모리 영역들로 구성된다.
런타임 데이터 영역은 다음의 다섯가지 영역으로 나눌 수 있다.
여기서 Method Area와 Heap Area는 모든 스레드가 공유하는 영역이고,
나머지 Stack Area, PC Register, Native Method Stack은 위 그림처럼 스레드마다 생성되는 개별 영역이다.
메서드 영역는 JVM이 시작될 때 생성되는 공간으로 바이트코드 파일(.class)를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 메모리 공간이다.
JVM이 동작하고 클래스가 로드될 때 적재되서 프로그램이 종료될 때까지 저장되며,
모든 스레드가 공유하는 영역으로 다음과 같은 초기화 코드 정보들이 저장된다.

이러한 데이터들은 위에서 설명한 클래스 로더에 의해 동적으로(실행하면서 그때그때 필요한 클래스들만) 적재되며, 때문에 메서드 영역에는 어플리케이션에 사용되는 모든 클래스의 메타데이터가 항상 저장되는 것이 아님.
각 클래스 혹은 인터페이스마다 별도의 Constant Pool 테이블이 존재하는데, 클래스를 생성할 때 참조해야할 정보들을 상수로 가지고 있는 영역이다.
런타임 상수 풀은 클래스 로더가 메서드 영역에 클래스를 로딩할 때 같이 메서드 영역에 적재되는 부분으로
클래스 및 인터페이스의 상수 뿐만 아니라 메서드와 필드에 대한 모든 참조에 대한 정보를 가지고 있다.
저번 클래스 로더 게시물을 통해 우리는 클래스 로더가 .class 파일을 JVM에 로드할 떄 Linking 과정의 resolve 단계에서 심볼릭 레퍼런스를 실제 레퍼런스로 바꾼다는 것을 알고 있다.
이러한 심볼릭 레퍼런스(참조하는 클래스가 메서드 영역에 할당되어 있지 않은 경우, 초기화 값임) 혹은 실제 레퍼런스(참조하는 클래스가 메서드 영역에 할당되어 있는 경우)가 담겨 있는 영역을 런타임 상수 풀이라고 볼 수 있다.
또한 메서드와 필드에 대한 모든 레퍼런스까지 담고 있어, 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아 참조한다.
String Pool은 힙 영역에 존재하는 특수한 메모리 영역으로 동일한 문자열 리터럴을 재사용하기 위해 사용됨
Java는 기본 자료형을 제외한 객체를 만들 때, new 키워드로 만들어 참조형으로 만든다.
하지만, String은 예외적으로 new 키워드 없이도 객체를 만들 수 있다. 이를 “문자열 리터럴 생성 방식”이라고 함
String str1 = "Hello"; // 리터럴 생성 방식, String Pool에 저장
String str2 = "Hello"; // 이미 존재하는 "Hello"를 참조
String str3 = new String("Hello"); // Heap에 별도 저장
Java 8 버전부터는 런타임 상수 풀은 "Metaspace" 영역(힙 외부에 존재하는 운영체제가 괸리하는 영역)에 저장되고, String Pool은 힙 영역에 저장되며 GC의 대상이 된다.
힙 영역은 메서드 영역과 함께 모든 스레드가 공유하며, JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임시 동적으로 할당하여 사용하는 영역.
자바에서는 “new” 키워드로 객체와 배열을 생성하며, 생성된 객체와 배열의 크기에 따라 힙 영역의 크기가 동적으로 변하고, 모든 스레드에서 공유되는 특징을 가진다.
힙 영역에 생성된 객체와 배열은 “Reference Type”으로서, JVM 스택 영역의 변수나 다른 객체의 필드에서 참조된다.
즉, 힙의 참조 주소는 각 스레드의 Stack이 갖고 있고, 해당 주소로 찾아가 힙 영역에 있는 인스턴스를 핸들링할 수 있다는 것이다.
만일 참조하는 변수나 필드가 없다면 의미 없는 객체가 되기 때문에, 이는 Garbage Collector의 수집 대상이 되어 힙 영역에서 자동으로 제거된다.
이처럼 힙 영역은 가비지 컬렉션(GC)의 대상이 되는 공간이며, 효율적인 가비지 컬렉션을 수행하기 위해 영역을 다음과 같이 나눈다.
(세부 내용은 JVM 실행 엔진 게시글 참조)
또한, 힙 영역에 관련하여 알면 좋은 영역이 있는데, 바로 “Metaspace”라는 영역이다.
Java 8 이후의 힙 영역의 구조는 정확히는 Young / Old Generation 만 포함된다.
Java 8 이전까지는 클래스의 메타데이터(클래스 정의, 메서드 정보, 클래스 변수 등)가 힙 영역의 “PermGen” 영역이라는 곳에 저장되었다.
이 경우에는 JVM 시작시, 설정된 고정 크기에 많은 클래스 로드와 많은 정적 데이터를 사용하는 경우 PermGen 영역이 고갈되면서 java.lang.OutOfMemoryError: PermGen space 오류로 이어지곤 했다.
그러나, Java 8 이후부터는 Metaspace라는 운영체제가 관리하는 네이티브 메모리 영역으로 분리하여 JVM이 메모리 요구사항에 따라 필요한 만큼 동적으로 메모리를 할당 받을 수 있게 되었다.
스택 영역은 메서드 호출 시 지역 변수, 매개변수, 함수 호출 내역 등이 저장되는 영역이다.
각 스레다마다 개별적으로 생성되는 영역이며, 메서드 호출 시마다 각각의 스택 프레임(그 메서드만을 위한 공간)이 생성되고, 메서드 수행이 끝나면 해당하는 스택 프레임은 삭제된다.

스택 프레임에는 아래와 같은 데이터들이 저장됨
단, 데이터의 타입에 따라 스택과 힙에 저장되는 방식이 다르다는 점에 유의해야함.
스택 영역은 각 스레다마다 하나씩 존재하며, 스레드가 시작될 때 할당됨
프로세스가 메모리에 로드될 때 스택 사이즈가 고정되어 있어, 런타임 간에는 스택 사이즈를 변경할 수 없다.
만일 고정된 크기의 JVM 스택에서 프로그램 실행 중 메모리 공간이 모자르다면 StackOverFlowError가 발생한다.
또한, 새로 생성될 쓰레드에게 메모리가 부족해 스택을 할당할 수 없거나, 스택의 확장에 충분한 메모리를 확보할 수 없는 경우 OutOfMemoryError 가 발생한다.
쓰레드를 종료하면 런타임 스택도 사라진다.
PC 레지스터 영역은 각 스레드마다 할당되며, 각 스레드의 현재 수행 중인 JVM 명령의 주소가 저장되는 영역이다.
일반적으로 프로그램의 실행은 CPU에서 명령어(Instruction)을 실행하는 과정으로 이루어지는데, CPU는 연산을 수행하는 동안 필요한 정보를 레지스터라는 CPU 내의 기억장치를 이용한다.

예를 들어, “add(A, B)”라는 명령어를 수행할 때, A와 B를 가져오고 이를 더하라는 연산이 순서대로 진행될 때,
CPU는 A 값을 로드하고 B 값도 로드하는 동안 A의 값을 CPU가 기억할 필요가 생긴다.
이 떄, 사용되는 저장공간이 CPU내의 Register이다.
그렇다면, JVM에서는 왜 또 별도로 PC Register라는 것을 사용할까?
첫번째로, 멀티쓰레딩 환경을 지원하기 위함이다.
스레드는 각자의 메소드를 실행하며, 멀티스레드 환경을 보장하기 위해 JVM에서는 각 스레드별로 수행할 명령어의 주소를 알 필요가 있다.
두번째로는 자바 또한 OS의 입장에서는 하나의 프로세스라는 것이다.
자바는 플랫폼으로부터 독립성을 추구하는 언어로 JVM이라는 가상머신으로 OS 위에서 가동되며, 따라서 가상머신의 자원을 활용하여야 한다.
떄문에 자바는 CPU Register를 이용하여 CPU에 직접 연산을 수행하도록 하는 것이 아닌,
현재 작업하는 내용을 CPU에게 연산으로 제공해야하며, 이를 위한 버퍼 공간으로 PC Register라는 CPU Register와 별개의 메모리 영역을 만들게 된 것이다.
자바는 스레드마다 수행하는 메소드의 JVM 명령(Instrution)의 주소를 PC Register로 저장하는데,
만일 자바가 아닌 다른 언어(C, 어셈블리)의 메소드를 수행하고 있다면, 해당 명령어의 주소는 저장하지 않고, “undefined” 값을 기록한다.
이러한 undefined 상태의 메소드들을 “네이티브 메소드”라고 부르며 이는 성능 향상에 도움을 주기도 한다.
자바 이외의 언어로 작성된 네이티브 코드(C, C++, 어셈블리)를 실행하기 위한 공간으로, JIT 컴파일러에 의해 변환된 네이티브 코드 역시 여기서 실행된다고 할 수 있다.
사용되는 메모리 영역으로는 일반적인 C 스택을 사용한다.
일반적으로 메서드를 실행하는 경우 JVM 스택에 쌓이다가 해당 메서드 내부에 네이티브 방식을 사용하는 메서드가 있다면 해당 메서드는 네이티브 스택에 쌓인다.
그리고, 네이티브 메서드 수행이 끝나면 다시 자바 스택으로 돌아와 다시 작업을 수행한다.
이 덕분에, 네이티브 코드로 되어 있는 함수의 호출을 자바 프로그램 내에서도 직접 수행할 수 있고 그 결과를 받아올 수도 있는 것이다.
네이티브 메서드 스택은 JNI(Java Native Interface)를 통해 자바와 네이티브 코드 간의 상호 작용을 지원하는데, JNI는 자바 어플리케이션에서 네이티브 코드를 호출할 수 있게 해주는 인터페이스라고 할 수 있다.
[ JNI (Java Native Interface) ]
JNI는 자바가 다른 언어로 만들어진 어플리케이션과 상호 작용할 수 있는 인터페이스를 제공하는 프로그램이다.
JNI는 Native Method를 적재하고 수행할 수 있도록한다.
자바 코드상에서native키워드로 선언된 메소드는 자바가 아닌 네이티브 코드로 구현되며,
JNI가 이렇게 작성한 자바 메서드와 네이티브 코드 간의 상호작용을 가능하게 한다.public class NativeExample { static { // C/C++ 라이브러리(네이티브 메서드 라이브러리) 로드 System.loadLibrary("NativeLib"); } public native void nativeMethod(); // 네이티브 메서드 선언 public static void main(String[] args) { NativeExample example = new NativeExample(); example.nativeMethod(); // 네이티브 코드 호출 } }
https://inpa.tistory.com/entry/JAVA-☕-JVM-내부-구조-메모리-영역-심화편#런타임_데이터_영역_runtime_data_area
https://velog.io/@ddangle/Java-런타임-데이터-영역Runtime-Data-Area에-대해