프로그래밍 언어/Java

JVM(1) - JVM 작동 과정

Julie825 2026. 4. 5. 15:35

Spring Boot로 서버를 운영하다가 배포과정에서 "Spring은 특성상 웜업이 오래걸린다" 하는 이야기를 들었다. 
Spring이 싱글톤 빈을 등록하여 사용한다는 것을 알고 있었기에 언어 문제라기보다는 프레임워크 특성이 크다고 생각했는데, 이번에 JVM 동작과정을 살펴보면서 JVM 특성도 이러한 성능 악영향에 영향을 미쳤겠다는 생각이 들어서 정리해보려고 한다.

 

1. .class 파일은 기계어가 아니다

코딩테스트 등을 준비하면서 단일 자바파일을 커맨드로 처리하다보면, java는 .class 형태의 컴파일 결과물을 생성하고, 이를 실행하다는 것을 알게된다.

$ javac Main.java
$ ls 
>> Main.class
$ java Main
>> Hello World

 

여기서 .class 확장자는 Java Bytecode를 담고있는데, Bytecode는 자바 프로그램을 JVM이 해석할 수 있을 정도의 저수준 언어로 번역한것이다. JVM 환경에서 동작한다고 하는 Kotlin, Groovy, Scala 등도 컴파일 결과로 Bytecode 파일을 생성해낸다.

그런데 c가 컴파일 결과로 machine native code를 생성해내는 것과 다르게, javac가 생성해내는 .class 파일은 컴퓨터가 바로 해석할 수 있는 형태는 아니다. 따라서 이 파일을 실제로 실행하려면 JVM이 런타임에 기계어 번역을 수행해줘야한다. 이 과정이 따로 일어나기에 Java 실행파일이 여러 OS, 하드웨어에 걸쳐 실행될 수 있게된다. 

2. JVM의 ByteCode -> Native Machine Code 번역과정


먼저 JVM이 정확히 뭔지 정의하자면, OS 등 실행환경에 관계없이 Java bytecode를 실행할 수 있게 정의한 가상 컴퓨터 명세이다. CPU,메모리의 활용 등을 정의하고있다. 예를 들면 JPA와 Hibernate의 관계에서 JPA에 해당하는 역할을 한다고 볼 수 있다.

추후 소개할 HotspotVM과 GraalVM이 JVM의 대표적인 구현체이다. 

모든 JVM은 명세에 따라 아래와 같은 JVM memory를 활용하여 bytecode를 실행하게 된다.

JVM Runtime Data Areas : 논리적으로 Memory 활용 방식을 명세한것


한번 커맨드라인에서 java Main 이 호출된 상황을 가정해보자.
런타임에 JVM은 재귀적으로 Loading, Linking, Initialization 세가지 단계를 거치며 클래스를 메모리에 적재하고 실행하게된다.

 

Step 1) Loading 

Loading은 .class의 클래스 정보를 method area에 적재하는 과정이다.

보통 최초 진입점으로 지정된 Main 클래스부터 시작해서, 뒤의 단계에서 참조하는 다른 클래스를 순차적으로 로딩하게된다.
Loading 단계를 세부적으로 나누면 다시 세단계로 나뉜다.

A) .class 파일 파싱 및 구조 검증
B) 상위 class, interface가 있다면 재귀적으로 Loading 재시도
C) 클래스 정보를 실제로 Method Area에 적재

이때 C스텝에서는 ClassLoader를 통해 해당 클래스를 위한 추가적인 bytecode를 받아온다.
ClassLoader의 구현체는 다양하며, 구현에 따라 외부 네트워크를 통해 코드를 가져오는 경우도 있다.
이렇게 ClassLoader에 의해 클래스 정보가 모두 채워지면 Method area 영역에 메타데이터 정보를 업로드한다.

메타데이터엔 클래스를 정의하는데 사용된 ClassLoader의 정보도 포함된다.

 

Step 2) Linking

Loading 단계가 완료된 후, Linking 단계에서는 Method area의 클래스 정보를 기반으로 클래스를 실행가능한 형태로 만든다.

Linking 단계가 마무리되어야 다음 스텝인 Initialization을 시작할 수 있다. 이 단계도 세분화하면 세단계로 나뉜다.

A) Verification
ClassLoader에 의해 갱신된 클래스파일의 최종본을 검증하는 과정이다.
Loading과정의 A)verification은 .class 파일의 포맷, 버전, 상속규칙 정도를 검증하는 정도였다면,
Linking에서의 verification은 이 코드가 JVM을 망가뜨리지 않을지 검증한다.
스택오버플로우, 메모리참조 유효성, VerifyError 포함 여부가 이때 검증된다. 

B) Preparation
이제 안전하다고 검증된 클래스 내용을 바탕으로 static 필드를 생성하고 기본값으로 초기화한다.
이때 메모리 공간만 할당하고, 코드를 실행하는 것은 아니다.

 

Step2) Linking > Resolution 시점에 Runtime Constant Pool의 symbolic referece를 실제값으로 결정한다.

 

C) Resolution
Runtime constant pool에 있는 symbolic reference를 실제 reference로 바꾸는 단계이다.
참조하는 다른 클래스가 있다면 다시 Step2) Loading 단계로 돌아가서 재귀적으로 호출하게된다.
구현에 따라 Step3 Initialization을 먼저 거친 이후 Lazy resolution 과정이 이루어질 수도 있다.

 

Step 3) Initialization

New 지시어, reflection 호출, Main 클래스 초기화 등의 이벤트에서 초기화가 트리거된다.
클래스 정보를 바탕으로 static 필드에 실질적인 값을 할당하는 과정이다.

 

3. JIT와 인터프리터

위 명세에 따라 JVM 동작을 구현한 대표적인 예시가 바로 HotSpotVM이다.
HotSpotVM은 단순히 호출시점에 bytecode를 Native Machine Code로 번역할 뿐만 아니라, 실행 중 수집한 정보를 기반으로 코드를 점진적으로 최적화하는 계층형 컴파일 구조를 채택하여 최적화도 진행한다.

 

HotspotVM에서 Native Machine Code를 생성하는 주체는 크게 두가지가 있다.

계층형 컴파일 도식



첫째는 인터프리터로, 초반에 bytecode를 한줄씩 해석하면서 실행한다. JS와 Python에 사용되는 인터프리터와 같이 동작한다고 볼 수 있다. 인터프리터는 별도의 컴파일 과정 없이 즉시 실행이 가능하기 때문에 빠른 시작 속도를 제공한다. 또한 단순히 프로그램을 실행하는것에 그치지 않고, 각 메서드의 호출 패턴을 프로파일링하는 역할도 한다.

둘째는 JIT 컴파일러로, 빈번히 사용되는 HotSpot으로 확인된 메서드를 최적화된 기계어 코드로 변환하는 역할을 한다.
JIT 컴파일러에 의해 변환된 코드는 Code Cache 영역에 저장되어 빠르게 실행 가능한 상태가 된다.


HotSpot VM의 JIT 컴파일러는 C1과 C2 두 가지로 구성되어 있으며, 각각 역할이 다르다.  
C1 컴파일러는 비교적 빠르게 컴파일을 수행하며, 기본적인 최적화와 함께 추가적인 profiling 정보를 수집하는 데 초점을 둔다. 

반면 C2 컴파일러는 더 많은 실행 데이터를 기반으로 인라이닝, escape analysis 등 고급 최적화를 수행하여 최종적인 실행 성능을 극대화한다.

이 두 컴파일러는 독립적으로 선택되는 것이 아니라, 계층형 컴파일 구조를 통해 단계적으로 사용된다. 프로그램은 먼저 인터프리터로 실행되다가, 일정 수준 이상 반복 실행되는 코드가 감지되면 C1 컴파일러에 의해 컴파일되고, 이후 충분한 실행 데이터가 쌓이면 C2 컴파일러에 의해 한 번 더 최적화된다.

이러한 구조를 통해 JVM은 초기 실행 속도와 장기 실행 성능을 동시에 확보할 수 있으며, 프로그램의 실행 패턴에 따라 런타임 중에도 지속적으로 최적화를 수행한다. 다만 코드들이 즉시 JIT 컴파일러에 의해 최적화되는 것이 아니기에, 위에서 지적했듯이 Spring Warmup 등이 발생하는 원인이 되기도 한다.

 

이번 글에서는 Java 실행 흐름과 JVM의 동작 과정, 그리고 Hotspot VM의 간단한 특성에 대해 알아보았다. 다음 글에서는 GC의 작동 흐름에 대해 알아보자.


참고한 자료
- Oracle - JVM Specification
- HotSpotVM - Runtime overview
- Tiered Compilation in JVM

'프로그래밍 언어 > Java' 카테고리의 다른 글

JVM(3) - jstat으로 GC 동작 관찰하기  (0) 2026.04.11
JVM(2) - G1GC의 동작과정  (0) 2026.04.11