"Write Once, Run Anywhere" - Java의 이 철학을 가능하게 하는 것이 바로 JVM(Java Virtual Machine)입니다. 오늘은 Java 개발자라면 반드시 알아야 할 JVM의 동작 원리와 내부 구조를 자세히 알아보겠습니다.
JVM(Java Virtual Machine)은 Java 프로그램이 실행되는 가상 머신입니다. 물리적인 컴퓨터가 아닌 소프트웨어로 구현된 가상의 컴퓨터로, Java 바이트코드를 해석하고 실행하는 역할을 담당합니다.
JVM이 Java 프로그램을 실행하는 과정을 단계별로 살펴보겠습니다.
Java 소스코드(.java)
↓ [javac 컴파일러]
바이트코드(.class)
↓ [클래스 로더]
런타임 데이터 영역
↓ [실행 엔진]
기계어 실행
// HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("안녕하세요, JVM!");
}
}
javac 컴파일러가 위의 Java 소스코드를 바이트코드로 변환:
javac HelloWorld.java # → HelloWorld.class 생성
JVM은 크게 다음과 같은 구성 요소로 이루어져 있습니다:

클래스 로더는 .class 파일을 JVM 메모리에 로드하는 핵심 컴포넌트입니다.
// 예시: 클래스가 처음 사용될 때 로딩됨
public class Example {
public static void main(String[] args) {
// 이 시점에서 User 클래스가 로딩됨
User user = new User();
}
}
링킹은 3개의 하위 단계로 구성됩니다:
a) Verification (검증)
b) Preparation (준비)
public class StaticExample {
static int number; // 준비 단계에서 0으로 초기화
static String text; // 준비 단계에서 null로 초기화
static final int CONSTANT = 100; // 컴파일 타임에 이미 값이 결정됨
}
c) Resolution (해석)
public class InitializationExample {
static int count = 10; // 초기화 단계에서 10으로 설정
static {
System.out.println("static 블록 실행");
count = 20; // 초기화 단계에서 실행
}
}
Bootstrap Class Loader (최상위)
↓
Extension Class Loader
↓
Application Class Loader (기본)
↓
Custom Class Loader (사용자 정의)
Bootstrap Class Loader
java.lang.*, java.util.* 등 핵심 Java API 로드Extension Class Loader
$JAVA_HOME/lib/ext)Application Class Loader
실행 엔진은 클래스 로더가 메모리에 적재한 바이트코드를 실제로 실행하는 컴포넌트입니다.
바이트코드는 JVM이 이해할 수 있는 중간 언어입니다:
// Java 소스코드
public int add(int a, int b) {
return a + b;
}
// 컴파일된 바이트코드 (javap -c 명령어로 확인 가능)
public int add(int, int);
Code:
0: iload_1 // 첫 번째 매개변수를 스택에 로드
1: iload_2 // 두 번째 매개변수를 스택에 로드
2: iadd // 두 값을 더함
3: ireturn // 결과 반환
public class InterpreterExample {
public static void main(String[] args) {
// 이 루프는 처음에는 인터프리터로 실행됨
for (int i = 0; i < 1000; i++) {
calculateSomething(i);
}
}
static int calculateSomething(int x) {
return x * x + 2 * x + 1;
}
}
JIT 컴파일러의 최적화 과정:
public class JITExample {
public static void main(String[] args) {
// 이 메서드가 충분히 많이 호출되면 JIT 컴파일 대상이 됨
for (int i = 0; i < 10000; i++) {
heavyCalculation(i); // Hot Spot으로 감지될 가능성
}
}
static long heavyCalculation(int n) {
long result = 0;
for (int i = 0; i < n; i++) {
result += i * i;
}
return result;
}
}
C1 컴파일러 (Client 컴파일러)
C2 컴파일러 (Server 컴파일러)
Tiered Compilation (계층 컴파일)
가비지 컬렉터는 더 이상 사용되지 않는 객체를 자동으로 메모리에서 제거하는 JVM의 핵심 기능입니다.
public class GCExample {
public static void main(String[] args) {
for (int i = 0; i < 1000000; i++) {
// 매 반복마다 새로운 String 객체 생성
String temp = new String("임시 문자열 " + i);
// temp는 루프가 끝나면 더 이상 참조되지 않음 → GC 대상
}
// 이 시점에서 GC가 작동하여 불필요한 String 객체들을 정리
}
}
Mark and Sweep 알고리즘
1. Mark: 사용 중인 객체들을 마킹
2. Sweep: 마킹되지 않은 객체들을 메모리에서 제거
3. Compact: 메모리 조각화 해결을 위한 압축 (선택적)
public class ReferenceExample {
public static void main(String[] args) {
// obj1과 obj2는 서로 참조 (순환 참조)
CircularRef obj1 = new CircularRef();
CircularRef obj2 = new CircularRef();
obj1.ref = obj2;
obj2.ref = obj1;
// 지역 변수를 null로 설정
obj1 = null;
obj2 = null;
// 순환 참조가 있지만, GC Root에서 도달할 수 없으므로 GC 대상
}
}
class CircularRef {
CircularRef ref;
}
GC Root 객체들:
# 힙 크기 설정 예시
java -Xms512m -Xmx2g MyApplication
# -Xms: 초기 힙 크기
# -Xmx: 최대 힙 크기
# GC 로그 설정 (Java 8)
java -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps MyApp
# GC 로그 설정 (Java 11+)
java -Xlog:gc*:gc.log:time MyApp
public class OptimizationTips {
// ✅ 좋은 예: 메서드가 자주 호출되어 JIT 최적화 대상
public static int fastCalculation(int x) {
return x * x + 2 * x + 1; // 간단한 연산
}
// ❌ 나쁜 예: 너무 큰 메서드는 JIT 최적화가 어려움
public static void hugeMothod() {
// 수백 줄의 코드...
// JIT 컴파일러가 최적화하기 어려움
}
// ✅ 좋은 예: final 메서드는 인라이닝 최적화 가능
public final int inlineableMethod(int x) {
return x + 1;
}
}
1편에서는 JVM의 전체적인 동작 과정과 클래스 로더, 실행 엔진, 가비지 컬렉터의 기본 개념을 다뤘습니다.
2편에서 다룰 내용:
JVM의 동작 원리를 이해하면 Java 애플리케이션의 성능 문제를 진단하고 해결하는 데 큰 도움이 됩니다. 특히 메모리 사용량이 많은 애플리케이션이나 고성능이 요구되는 시스템을 개발할 때 JVM에 대한 깊은 이해는 필수입니다.
다음 편에서는 JVM의 메모리 구조를 더 자세히 살펴보고, 실제 개발 현장에서 활용할 수 있는 최적화 기법들을 알아보겠습니다!