

2025년 현재 한국 취업 시장에서 대학생이 압도적으로 많다. 이는 대학생활만으로 취업하기 어렵다는 것을 반증한다. 그러므로 취업을 위하여 정보처리기사, SQLD 등의 기사 자격증은 필수적이다. 국무지원 부트캠프는 일반적으로 6개월간의 과정이므로 자바에 관한 심도있는 이해를 바라기는 어렵다.
따라서 우리는 자바의 JVM에 대한 심층적인 이해를 높이고자 이 문서를 작성한다. 이 가이드는 자바 개발자가 반드시 알아야 할 JVM의 핵심 개념부터 최신 기술 동향까지를 다룬다.
JDK(Java Development Kit): 자바 개발 환경으로 자바 어플리케이션을 개발하기 위해 필요한 도구를 제공한다(javac, javap 등). JDK는 JRE + 개발 도구라고 볼 수 있다.
JRE(Java Runtime Environment): 자바를 실행하기 위한 환경이며, JVM, 자바 클래스 라이브러리, 기타 실행용 파일을 포함한다.
JVM(Java Virtual Machine): 자바 가상 머신으로, 자바 프로그램을 실행하기 위한 가상 머신이다. 운영체제에 종속되지 않게 자바 프로그램을 실행할 수 있게 해준다. 물론 JVM 자체는 운영체제 종속적이며(운영체제에 맞춰 설치해야 함), 이것이 바로 "Write Once, Run Anywhere"의 핵심이다.
자바 프로그램이 실행되는 전체 과정을 시각적으로 이해하면, 이후의 세부 내용을 더 쉽게 파악할 수 있다.

이 과정의 각 단계를 상세히 살펴보자.
런타임 이전(컴파일 타임)에 자바 소스 코드(.java)를 컴파일러(javac)를 통해 바이트코드(.class)로 변환하는 과정이다.
바이트코드는 자체는 바이너리 파일이다. 즉 인간이 파악하기 어려운 이진수로 구성되어 있다. 명령어 1바이트(8비트)를 사용하여 "바이트코드"라고 불리며, 그 외의 데이터는 명령어로 조작할 대상의 포인터나 데이터를 담는다. 바이트코드는 Symbolic Reference를 사용한다.
기계어: 기계가 직접 이해할 수 있는 이진수를 의미한다. 하드웨어에 종속적인 것이 특징이다(CPU 명령어 체계에 따라 달라진다).
어셈블리어: 기계어는 이진수로 되어있기에 인간이 보기에 어렵다. 이를 극복하고자 인간이 읽고 쓰기 쉽도록 1대1 대응하는 언어로 작성한 것이 어셈블리어다. 기계어와 1대1 대응하기에 마찬가지로 하드웨어 종속적이다.
자바 바이트코드: CPU가 직접 알아들을 수 없기에 기계어는 아니다. 어셈블리어처럼 사람이 읽을 수 있는 형태로 변환할 수 있다. 그러나 자바 바이트코드는 자바의 철학에 따라 하드웨어 종속적이지 않다는 것이 가장 큰 특징이다.
바이트코드를 역어셈블러(javap)로 해석해서 보면 다음과 같은 형태로 볼 수 있다:
public void example();
Code:
0: getstatic #2
3: ldc #3
5: invokevirtual #4
8: return
주의: IDE에서 .class 파일을 열면 역컴파일된 자바 코드를 보여주는데, 실제 .class 파일은 바이너리 형식이므로 완전히 다르게 생겼다. 바이트코드가 저렇게 생겼다고 착각하면 안 된다!
JVM이 컴파일된 바이트코드를 읽고 메모리에 로드를 수행하는 과정이다. 자바 프로그램을 실행하기 위해 필요한 클래스와 객체들을 메모리(Run Time Data Area)에 옮기는 것이다.
코드를 구현하다 보면 중복을 줄이기 위해 상속을 사용하게 된다. ClassLoader도 계층적으로 생성이 가능하다. 클래스 로더는 다음과 같은 계층 구조를 가지고 있다.

부모 클래스 로더가 우선권을 가지고 있으므로, 자식 클래스 로더는 부모 클래스 로더에 먼저 요청을 위임한다. 따라서 ClassLoader3 → ClassLoader2 → ClassLoader1 순서로 요청이 이루어진다.
ClassLoader에는 클래스 언로딩(Unloading) 기능이 없다. 언로딩을 하기 위해서는 ClassLoader 자체를 삭제하고, ClassLoader을 다시 생성하는 방법이 있다.
부트스트랩 클래스 로더: JVM을 기동할 때 생성되며, Object 클래스들을 비롯하여 자바 API들을 로드한다. 다른 클래스 로더와 달리 자바가 아니라 네이티브 코드로 구현되어 있다.
익스텐션 클래스 로더(Extension Class Loader): 기본 자바 API를 제외한 확장 클래스들을 로드한다. 다양한 보안 확장 기능 등을 여기에서 로드하게 된다.
시스템 클래스 로더(System Class Loader): 부트스트랩 클래스 로더와 익스텐션 클래스 로더가 JVM 자체의 구성 요소들을 로드한다면, 시스템 클래스 로더는 애플리케이션의 클래스들을 로드한다. 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드한다.
사용자 정의 클래스 로더(User-Defined Class Loader): 애플리케이션 사용자가 직접 코드 상에서 생성해서 사용하는 클래스 로더이다.
런타임 데이터 영역은 JVM이 프로그램을 실행하는 동안 사용하는 메모리 영역이다. 이 영역은 여러 개의 섹션으로 나뉜다.

Heap Area: JVM당 하나만 존재하며 모든 스레드가 공유한다. Java로 구성된 객체와 JRE 클래스들이 올라간다. 문자열에 대한 정보를 가진 String Pool, 실제 데이터를 가진 인스턴스, 배열 등이 여기에 저장된다. GC의 주요 대상이다.
Method Area(Metaspace): Java 8부터는 Metaspace라고 불린다. JVM당 하나만 존재하며 스레드가 모두 공유한다. 인스턴스 생성을 위한 객체 구조, 생성자, 필드 등이 저장되고, Runtime 상수풀과 정적(static) 변수, 메서드 데이터와 같은 클래스 데이터가 여기에 저장된다.
Constant Pool:
PC Register (Program Counter Register): 프로그램 카운터 레지스터로서 현재 실행 중인 명령어의 주소가 기록된다(실제 물리주소).
Native Method Stack: 자바로 작성된 프로그램 실행 시 순수하게 자바로만 구성된 코드를 사용할 수 없는 시스템의 자원이나 API가 존재한다. 이런 다른 언어로 작성된 메서드들을 Native Method라고 한다. Native Method가 사용하는 스택이다.
JVM Stack: 메서드가 호출될 때마다 Frame들을 저장하는 일종의 function call stack이다. 메서드가 완료되면 프레임이 pop된다.
JVM의 바이트코드를 해석해서 기계어로 변환해 실행하는 과정이다. 기본적으로 Execution Engine(실행엔진)의 인터프리터와 JIT 컴파일러가 수행하며, Native 코드의 경우 Native Method Interface, Native Method Library가 수행하게 된다.

JIT 컴파일러는 인터프리터의 단점을 보완하기 위해 자주 실행하는 명령어(HotSpot이라는 표현을 씀)인 경우 실시간으로 컴파일을 추가 진행해 미리 기계어로 바꿔준다. JVM의 속도향상을 위한 보조 해석 방법이다.
JIT 컴파일이 성능을 높이는 이유:
자바의 JVM에서는 가비지 컬렉터가 불필요한 메모리를 알아서 정리해주어 메모리 누수를 방지해주고 메모리를 청소해준다. 이것이 자바의 가장 큰 장점 중 하나다.

가비지 컬렉션을 실행하기 위해 JVM이 애플리케이션의 실행을 멈추는 작업이다. GC를 실행하는 쓰레드를 제외한 모든 쓰레드들의 작업이 중단되고, GC가 완료되면 작업이 재개된다.
Mark (마크): 사용되는 메모리와 사용되지 않는 메모리를 식별하는 작업이다. 모든 객체를 순회하면서 어떤 객체가 여전히 사용 중인지를 판단한다.
Sweep (스윕): Mark 단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업이다. 사용되지 않는 객체들이 점유하고 있던 메모리를 회수한다.
Compact (컴팩트): 메모리 단편화를 해결하는 작업이다. Sweep 이후 흩어져 있는 메모리를 연속으로 모아서 새로운 객체 할당 공간을 확보한다.
메모리를 사용 후 해제하게 되면 메모리 사이에 빈 공간이 생기게 된다. 이렇게 되면 총 12K의 여유 메모리가 있음에도 5K가 넘는 작업은 실행할 수 없다. 이런 현상을 메모리 단편화라고 한다. 이를 해결하기 위해서는 사용 중인 메모리를 연속으로 모으고 나머지 공간을 온전히 사용할 수 있도록 해야 한다.
현대의 모든 JVM GC는 세대별 컬렉션 이론에 기반한다:
약한 세대 가설: 대다수 객체는 일찍 죽는다. 실제 통계에 따르면 신세대 객체의 98%가 첫 번째 GC에서 회수된다.
강한 세대 가설: GC에서 살아남은 횟수가 늘어날수록 더 오래 살 가능성이 커진다.
세대 간 참조 가설: 세대 간 참조의 개수는 같은 세대 안에서의 참조보다 훨씬 적다.
Young Generation (신세대):
Old Generation (구세대):