JVM 실행 구조와 메모리 영역 이해하기

JVM이 바이트코드를 실행하는 방식과 Class Loader, 실행 엔진, 런타임 메모리 영역, GC 튜닝 관점을 정리합니다.

2026-08-14 · 최초 발행 2025-12-30

바이트코드와 운영체제 사이에 놓인 JVM

JVM(Java Virtual Machine)은 자바 프로그램이 운영체제와 분리된 방식으로 실행되도록 .class 바이트코드를 메모리에 올리고 처리하는 가상 머신이다. 자바의 "Write Once, Run Anywhere"는 같은 바이트코드를 각 플랫폼의 JVM이 실행한다는 구조에서 나온다.

이 과정에는 Class Loader, Execution Engine, Runtime Data Area, Garbage Collector가 관여한다. 바이트코드를 네이티브 코드로 바꾸는 데 따른 오버헤드는 존재하지만, JIT 컴파일러와 여러 최적화 기법이 이를 줄인다.

Java 소스코드(.java)Java 컴파일러(javac)바이트코드(.class)JVM(Windows)JVM(Linux)JVM(macOS)실행Write Once,Run Anywhere

JVM은 컴파일러가 만든 .class 파일을 로드하고, 바이트코드를 기계어로 변환해 실행한다. 메모리를 자동으로 할당하고 Garbage Collection으로 사용하지 않는 객체를 정리하며, 메모리 누수를 방지하는 역할도 맡는다. 샌드박스 환경과 바이트코드 검증을 통해 악의적 코드의 격리도 지원한다.

실행 방식이 성능과 이식성을 함께 결정한다

인터프리터는 바이트코드를 한 줄씩 해석해 실행한다. 시작은 빠르지만 실행 자체는 느릴 수 있다. JIT 컴파일러는 자주 실행되는 코드를 네이티브 코드로 컴파일하고 캐시해 재사용함으로써 실행 속도를 높인다. AOT 컴파일러는 네이티브 코드 변환을 미리 수행하는 방식이다.

변환에는 시간이 들며 C/C++ 네이티브 코드와 비교하면 성능 오버헤드가 생길 수 있다. JVM은 최적화를 통해 이 격차를 축소한다.

운영체제 독립성은 JVM이 설치된 환경에서 같은 바이트코드를 실행할 수 있다는 뜻이다. Windows, Linux, macOS, Solaris 등 플랫폼마다 JVM 구현은 다르지만 애플리케이션 코드를 재사용할 수 있다. 유지보수와 배포가 단순해지는 반면 JVM 설치가 필요하고 플랫폼 고유 기능에 접근하는 데는 제약이 있다.

클래스는 로드·링크·초기화를 거쳐 실행 준비를 마친다

Class Loader는 .class 파일을 메모리로 가져오고 바이트코드를 검증한 뒤 클래스를 초기화한다.

Loading 단계에서는 클래스 파일을 찾아 메모리에 적재한다. 이어지는 Linking은 검증, 준비, 해석으로 나뉜다. Verification은 바이트코드의 유효성을 검사하고, Preparation은 static 필드의 메모리를 할당하며 기본값을 초기화한다. Resolution은 심볼릭 참조를 직접 참조로 바꾼다. 마지막 Initialization에서 static 블록을 실행하고 static 변수를 초기화한다.

Class Loader 계층도 역할이 구분된다. Bootstrap Class Loader는 rt.jar를 포함한 코어 Java 클래스를 로드하고, Extension Class Loader는 확장 라이브러리를 담당한다. Application Class Loader는 애플리케이션 클래스패스의 클래스를 로드한다.

Execution Engine과 런타임 데이터 영역

Execution Engine에는 인터프리터, JIT 컴파일러, Garbage Collector가 포함된다. 인터프리터는 바이트코드를 한 줄씩 해석해 실행한다. JIT 컴파일러는 Hot Spot으로 판단된 자주 실행되는 코드를 네이티브 코드로 컴파일하고 캐시한다. Garbage Collector는 더 이상 사용되지 않는 객체를 제거해 메모리를 회수한다.

Runtime Data Area는 데이터의 성격과 스레드 공유 여부에 따라 나뉜다.

Method Area에는 클래스 메타데이터, static 변수, 메서드 정보가 저장되며 모든 스레드가 공유한다. Heap도 모든 스레드가 공유하는 영역으로 객체 인스턴스가 놓이고 Garbage Collection의 대상이 된다. Heap은 Young Generation과 Old Generation으로 구분된다.

Stack은 메서드 호출 때 프레임을 만들며 지역 변수, 매개변수, 리턴 값을 보관한다. 스레드별로 독립적이다. PC Register 역시 스레드별로 분리되어 현재 실행 중인 JVM 명령어 주소를 가리킨다. Native Method Stack은 JNI(Java Native Interface)를 통한 네이티브 메서드 호출에 사용되며 스레드별 독립 영역이다.

객체 수명과 호출 흐름을 담는 메모리

새 객체는 Young Generation의 Eden에 할당된다. Minor GC 뒤에도 남은 객체는 Survivor 0 또는 Survivor 1에 위치하며, Eden과 Survivor 영역의 비율은 일반적으로 8:1:1이다. Young 영역에서 오래 살아남은 객체는 Old Generation으로 이동하고, 이 영역은 Full GC 대상이며 크기가 크다.

클래스 메타데이터는 Java 7 이하에서 Permanent Generation에, Java 8+에서는 Metaspace에 저장된다. Java 8부터 Metaspace는 네이티브 메모리를 사용한다.

Stack 프레임은 메서드마다 생성된다. Local Variable Array는 지역 변수를, Operand Stack은 연산을, Frame Data는 상수 풀 참조와 예외 처리 정보를 보관한다. 메서드 호출 시 프레임이 쌓이고 종료 시 제거되는 후입선출(LIFO) 구조다.

JVM 메모리Heap(공유)Stack(스레드별)Method Area(공유)MetaspaceYoung Gen(Eden, S0, S1)Old Gen프레임(메서드 호출)클래스 메타데이터Static 변수

JIT와 GC를 관찰하며 조정하기

JIT 컴파일러는 자주 실행되는 메서드와 루프를 식별하고 실행 횟수를 카운팅한다. 임계값을 넘으면 해당 코드를 컴파일한다. 이 과정에서 인라인 확장(Inlining), 불필요한 코드 제거(Dead Code Elimination), 루프 최적화(Loop Unrolling), 탈출 분석(Escape Analysis)을 적용한다.

GC 튜닝에서는 -Xms로 초기 Heap 크기, -Xmx로 최대 Heap 크기, -XX:NewRatio로 Young/Old 비율을 설정한다. GC 알고리즘은 목적에 따라 고른다. Serial GC는 단일 스레드 방식이고, Parallel GC는 멀티 스레드로 처리량을 중시한다. CMS GC는 낮은 지연 시간을, G1 GC는 대용량 Heap을 대상으로 하며 Java 9+에서는 default다. ZGC와 Shenandoah는 초저지연을 목표로 한다.

운영 중에는 JVisualVM, JConsole, Java Mission Control을 통해 상태를 확인할 수 있다. JVisualVM은 실시간 모니터링에, JConsole은 JMX 기반 모니터링에, Java Mission Control은 프로파일링에 사용된다. Heap 사용률, GC 빈도 및 시간, 스레드 상태, CPU 사용률을 함께 봐야 JVM의 메모리와 실행 상태를 해석할 수 있다.

JVMJava운영체제가상 머신Garbage Collection