후다닥 공부한 내용 정리하고 과제 ,시험준비, 다음 주 코테 벼락치기하러 가겠습니다...
# 00. JVM Overview
Java의 등장 이전에 웹 애플리케이션 생태계에서는 문제가 하나 있었습니다.
OS나 CPU가 지원하는 스펙에 따라 컴파일 플랫폼에서는 제대로 동작하는 애플리케이션이 타겟 플랫폼에서는 동작하지 않는 경우가 있다는 것이었습니다.
쉽게 얘기해서 리눅스에서 컴파일한 프로그램이 윈도우 위에서는 동작하지 않는 문제가 있었다는 겁니다.
이러한 문제를 컴파일 시점에 타겟 플랫폼을 고려하는 크로스 컴파일 전략으로 극복하는 케이스가 있었지만, Java의 모토는 이와 달랐습니다.
Java는 플랫폼을 고려하지 않고 일단 코드를 작성하고 컴파일하면, 어디에서든 실행할 수 있는 환경을 구축하기를 원했고, 여기서 등장하는 개념이 JVM입니다.

JVM은 Java Virtual Machine의 약자이며, 직역하자면 '자바를 실행하기 위한 가상 기계'입니다.
하드웨어나 운영체제에 종속되지 않고 자바 코드가 동작할 수 있도록 OS와 애플리케이션 사이에 존재하는 환경이며, 자바로 작성된 애플리케이션은 모두 이 JVM 위에서만 실행되기 때문에, 자바 애플리케이션이 실행되기 위해서는 반드시 JVM이 필요하다고 볼 수 있습니다.
물론, 기존에 애플리케이션이 OS에 종속적이었듯, JVM도 역시 OS에 종속적이기 때문에 각 OS에 맞는 JVM을 설치해야 합니다.
# 01. JVM Architecture

JVM은 크게 런타임 데이터 영역 (Runtime Data Area), 실행 엔진 (Execution Engine), JNI (Native Method Interface), Native Method Library로 이루어져 있습니다.
저희가 작성한 자바 소스코드는 .java 형태로 저장되고 컴파일 타임 때 자바컴파일러(javac)에 의해. class의 바이트 코드로 변환됩니다.
이 바이트 코드는 기계가 바로 읽고 실행할 수 있는 형태의 언어가 아닙니다.
.class로 변환된 파일들을 Class Loader에 의해 JVM Runtime Data Area로 로딩을 시킨 후 Execution Engine의 Interpreter 혹은 JIT Compiler에 의해 기계어로 해석되어 실행하게 됩니다.
JVM을 이루고 있는 각 컴포넌트에 대해 알아보겠습니다.
## Class Loader
언급했다시피 클래스 로더는 javac에 의해 바이트 코드로 컴파일된 파일들을 런타임 때 JVM의 메모리 영역 Runtime Data Area로 로딩합니다.
이때, 클래스 로더는 모든 클래스 파일을 한 번에 메모리 영역에 올리지 않습니다.
Execution Engine이 필요한 클래스 정보를 클래스 로더에게 요청을 하고 동적으로 바이트 코드를 로드하게 됩니다.
클래스 로더가 바이트 코드를 로드하는 과정은 아래 세 단계로 나눌 수 있습니다.
로딩
로딩 단계에서는 자바 바이트 코드를 Runtime Data Area 내 메소드 영역에 저장합니다.
링킹
링킹 단계는 크게 세 가지로 나뉩니다.
- 검증 : 바이트코드가 제대로 자바의 규칙을 따르고 있는지 검증
- 준비 : 클래스가 필요로 하는 메모리 양 미리 할당
- 분석 : 클래스가 참조하는 객체에 실제 메모리 주소 값을 대입
초기화
초기화 단계는 클래스 내 스태틱 변수에 값을 할당하고 초기화하는 단계라고 볼 수 있습니다.
## Runtime Data Area
JVM이 운영되면서 운영체제로부터 부여 받은 메모리 영역을 Runtime Data Area라고 합니다.
Runtime Data Area는 Method Area, Heap, Java Stack, PC Register, Native Method Stack으로 이루어져 있습니다.
Method 영역과 Heap 영역은 모든 스레드가 공유하는 공간이며, 나머지 세 개는 스레드마다 하나씩 공유되는 공간입니다.
이번 글의 핵심인 JVM의 메모리 영역에 해당하는 내용이므로, #02. Runtime Data Area 부분에서 자세하게 다루겠습니다.
## Execution Engine
클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 읽고 실행하는 엔진을 Execution Engine이라고 합니다.
앞서 말씀 드렸듯, 컴파일이 끝난 class 파일의 바이트 코드는 기계가 직접 읽을 수 있는 상태가 아닙니다.
이를 런타임때 기계가 실행할 수 있는 형태로 JVM 내부에서 변경해 줍니다.
이 수행 과정에는 두 가지 종류가 있는데요, 바로 인터프리터와 JIT 컴파일러입니다.
이 둘의 차이는 인터프리티드 언어와 컴파일드 언어의 비교를 떠올려 비유할 수 있습니다.
파이썬이나 자바스크립트 같은 언어가 그렇듯, 클래스 파일의 바이트코드를 한 줄씩 읽어서 즉시 해석하고, 그 결과를 곧바로 CPU가 실행하도록 하는 것이 인터프리터이고요.
반대로 자바와 같은 언어처럼 모든 코드를 한 번에 미리 변환해서 저장하도록 하는 역할을 JIT 컴파일러가 수행하며, 이러한 특징이 해당 컴파일러의 이름이 JIT(Just-In-Time) 컴파일러인 이유라고 볼 수 있습니다.
추가적으로 Execution Engine 영역에는 인터프리터와 JIT 컴파일러와 더불어 우리가 흔히 Java의 강점 중 하나로 알고 있는 Garbage Collector가 있습니다.
GC만 하더라도 다룰 내용이 많기 때문에 GC에 대해서는 이후에 내용을 정리해서 포스트를 따로 작성하도록 하겠습니다.
## JNI (Java Native Interface) & Native Method Library
JNI는 자바가 다른 언어로 작성된 코드를 호출하거나, 반대로 네이티브 코드가 자바 메서드를 호출할 수 있도록 해주는 인터페이스 규약입니다.
Java 애플리케이션에서도 자바로만 구현하기 어려운 경우에, 기존 C/C++ 라이브러리를 그대로 활용하는 경우가 있는데 이때 사용한다고 볼 수 있습니다.
Native Method Library는 JNI를 통해 로드되는 네이티브 코드가 구현된 라이브러리를 칭합니다.
# 02. Runtime Data Area

Runtime Data Area는 아래 다섯 가지 영역으로 나누어져 있습니다.
1. Method Area
2. Heap
3. Java Stack
4. PC Register
5. Native Method Stack
다음 코드를 통해 각 영역에 어떤 데이터가 올라가는지 함께 살펴보겠습니다.
public class Kid {
public String name;
public String className;
private int age;
public Kid(String name, String className) {
this.name = name;
this.className = className;
this.age = 5;
}
}
public class Main {
public static void main(String[] args) {
Kid kid = new Kid("짱구", "해바라기반");
}
}
## Method Area
JVM이 시작될 때 운영체제에서 할당받은 메모리 영역 중 하나로, 클래스 로딩 후에 클래스의 구조(바이트코드)와 런타임 상수 풀, static 변수 등을 저장합니다.
모든 스레드가 공유하기 때문에, 한 번 로드된 클래스 정보는 어디서든 접근이 가능합니다.
구체적으로 저장되는 항목은 다음과 같습니다.
1. 클래스의 바이트코드
2. 런타임 상수 풀 (Constant Pool) - 문자열 리터럴, 메서드/필드 참조 정보 등
3. static으로 선언된 변수 영역
위 코드에서는 Kid 클래스 정보가 필요해질 때 Execution Engine으로부터 요청을 받고 Method Area에 정보가 올라간다고 이해할 수 있습니다.
## Heap
Heap은 JVM이 운영체제로부터 할당받는 메모리 영역 중, 모든 객체 인스턴스와 배열을 생성하고 관리하는 공간입니다.
Garbage Collector가 동작하여 더 이상 참조되지 않는 객체를 회수하는 대상이기도 합니다.
런타임 동안 크기가 변할 수 있고, 여러 GC 알고리즘이 적용되기도 합니다.
예시 코드에서는 kid 객체와 kid가 참조하는 String 리터럴 "짱구" 그리고 "해바라기반" 객체도 모두 힙 영역에 만들어진다고 이해할 수 있습니다.
## Java Stack
Java Stack은 메서드를 호출할 때마다 하나씩 생성되는 스택 프레임들이 쌓이는 공간입니다.
스택 프레임은 로컬 변수 테이블, 연산 스택, 프레임 데이터로 이루어져 있으며, 메서드가 호출될 때 스택 프레임을 스택에 push 하고 메서드가 종료될 때 pop 동작을 하게 됩니다.
public class Main {
public static void main(String[] args) {
Kid kid = new Kid("짱구", "해바라기반");
}
}
다시 이 예시 코드를 보면서 코드가 동작할 때의 스택 영역이 어떻게 동작하는지 확인해 보겠습니다.

1. main 함수가 실행되고 스택에 main에 대한 스택 프레임이 push 됩니다.
2. Kid 생성자가 실행이 되며 스택에 Kid 생성자 스택 프레임이 push 됩니다. (생성자도 메서드의 일종입니다!)
3. 생성자 메서드의 동작이 끝난 후 Kid 생성자 스택 프 레임이 pop 됩니다.
4. 이후 main 함수도 정상적으로 종료되면서 main 스택 프레임이 pop 됩니다.
## PC Register
레지스터는 CPU가 연산을 수행하는 동안 필요한 정보를 저장하는 기억장치를 말합니다.
모든 연산마다 필요한 값을 메모리에 올려두는 방법은 너무 비효율적이기 때문에, 임시로 CPU가 저장하고 있는 공간이라고 이해할 수 있습니다.
Program Counter Register는 JVM 스레드마다 하나씩 독립적으로 존재하는 레지스터로, 현재 실행하는 자바 메서드의 바이트코드 중에서, 어떤 명령어를 수행하고 있는지에 대한 인덱스(주소)를 관리합니다.
방금 말씀드린 CPU의 레지스터와 다른 점이 있다면, PC Register에서는 값을 직접 저장하고 있는 것이 아니라, 이 레지스터에 대한 정보가 어느 메서드의 바이트코드 배열의 몇 번째 바이트인지를 특정할 수 있는 참조값을 가지고 있다고 보실 수 있습니다.
이를 테면, Main 클래스의 main() 함수 바이트코드를 메서드 영역에 올려뒀다고 했을 때, 실제 메모리에는 바이트코드가 byte[] 형태로 저장되어 있습니다.
pc 레지스터에서는 이 바이트코드 배열에서 다음에 실행할 바이트코드 명령어의 오프셋이 기록된다고 이해할 수 있습니다.
## Native Method Stack
네이티브 메서드 스택은 자바 코드 상에 native 키워드를 사용해 선언된 메서드를 호출할 때, JVM이 해당 네이티브 메서드를 실행하기 위해 사용하는 별도의 스택 영역입니다.
순수 자바 바이트코드를 실행할 때는 이 영역을 사용하지 않지만, 네이티브 코드를 호출하면 JVM이 네이티브 메서드 스택 프레임을 생성하여 네이티브 함수로 제어를 넘깁니다.
네이티브 메서드 스택이 만들어졌다 사라지는 과정은 JVM 내부의 JNI 규약에 따라 진행됩니다.
이번에 JVM 메모리 구조를 공부하면서 비전공(어쩌면 유사전공)으로써 컴퓨터 공학적인 소양이 부족하다는 점을 느꼈습니다.
할 게 산더미네요.