자바 컴파일 과정 (Java Compile Process)

한눈에 보기

javac가 소스를 바이트코드로 바꾸고 JVM이 로딩, 링킹, 초기화 후 인터프리터와 JIT로 실행하는 전체 흐름을 정리한다. 클래스 파일 구조와 부모 위임 모델, 계층형 컴파일까지 다룬다.

최종 업데이트 2026.09.17

자바 컴파일 과정은 개발자가 작성한 .java 소스 파일을 자바 컴파일러(javac)가 바이트코드(bytecode)가 담긴 .class 파일로 변환하고, 이후 자바 가상 머신(JVM)이 그 바이트코드를 로딩하고 검증한 뒤 인터프리터와 JIT 컴파일러(Just-In-Time Compiler)로 실행하는 전체 흐름을 말한다. 소스 코드가 기계어로 한 번에 번역되는 C 계열 언어와 달리, 자바는 중간 언어인 바이트코드를 거치기 때문에 한 번 컴파일한 결과물을 어떤 운영체제의 JVM에서도 실행할 수 있다.

javac가 하는 일

javac는 소스 코드를 몇 단계에 걸쳐 바이트코드로 바꾼다. 먼저 어휘 분석(lexical analysis)으로 소스 문자를 키워드, 식별자, 리터럴 같은 토큰으로 쪼개고, 구문 분석(parsing)으로 토큰을 문법 규칙에 맞춰 추상 구문 트리(Abstract Syntax Tree)로 조립한다. 이어서 의미 분석(semantic analysis) 단계에서 타입 검사, 심벌 해석, 접근 제어 검증을 수행하고 애너테이션 처리기(annotation processor)를 실행한다. 마지막으로 트리를 순회하며 바이트코드를 생성해 클래스마다 하나의 .class 파일을 만든다.

이 과정에서 제네릭의 타입 소거(type erasure), 오토 박싱 치환, 문자열 연결 처리 같은 문법적 편의 기능이 실제 바이트코드로 구체화된다. javac는 최적화를 거의 하지 않는데, 상수 접기(constant folding) 정도만 수행하고 본격적인 최적화는 실행 시점의 JIT에 맡기는 것이 자바의 설계 철학이다.

클래스 파일과 바이트코드

.class 파일은 매직 넘버 0xCAFEBABE로 시작하는 정해진 구조를 갖는다. 클래스와 메서드와 문자열 리터럴 정보를 담는 상수 풀(constant pool), 필드와 메서드 정의, 각 메서드의 바이트코드 명령이 차례로 들어 있다. 바이트코드는 스택 기반 가상 머신의 명령어 집합으로, 피연산자 스택에 값을 올리고 내리며 계산한다. javap -c 명령으로 내용을 직접 확인할 수 있다.

public class Hello {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        System.out.println(a + b);
    }
}

// javap -c Hello 의 main 메서드 부분 (요약)
// iconst_1        상수 1을 피연산자 스택에 적재
// istore_1        지역 변수 1번에 저장
// iconst_2
// istore_2
// getstatic  java/lang/System.out
// iload_1
// iload_2
// iadd            스택의 두 값을 더한다
// invokevirtual   println 호출

클래스 로딩과 링킹

실행이 시작되면 JVM의 클래스 로더(class loader)가 필요한 클래스를 필요한 시점에 메모리로 가져온다. 이 과정은 로딩(loading), 링킹(linking), 초기화(initialization)로 나뉜다. 로딩은 .class 파일을 읽어 메서드 영역(method area), 즉 핫스팟 구현 기준 메타스페이스(Metaspace)에 클래스의 메타데이터를 올리고 힙에 해당 클래스를 대표하는 Class 객체를 만드는 단계다.

링킹은 다시 셋으로 나뉜다. 검증(verification)은 바이트코드가 JVM 명세를 어기지 않는지, 스택을 오염시키거나 접근 제어를 우회하지 않는지 확인한다. 준비(preparation)는 static 필드 메모리를 확보하고 타입 기본값으로 채운다. 해석(resolution)은 상수 풀의 심벌릭 참조를 실제 메모리 참조로 바꾸는 단계로, 대개 실제 사용 시점까지 지연된다. 마지막 초기화 단계에서 static 초기화 블록과 static 필드 대입이 실행되는데, 클래스가 처음 실제로 사용될 때 단 한 번만 일어난다. 클래스 로더는 부트스트랩, 플랫폼, 애플리케이션 로더가 계층을 이루며, 자식이 로딩 요청을 받으면 먼저 부모에게 위임하는 부모 위임 모델(parent delegation model)로 동작해 핵심 클래스의 위조를 막는다.

실행 엔진: 인터프리터와 JIT

로딩된 바이트코드는 처음에는 인터프리터가 한 명령씩 해석하며 실행한다. 실행 횟수 카운터가 임계치를 넘어 자주 불리는 메서드나 루프, 이른바 핫스팟(hotspot)이 발견되면 JIT 컴파일러가 그 부분을 네이티브 기계어로 컴파일해 캐시에 저장하고, 이후 호출부터는 기계어를 직접 실행한다. 핫스팟 JVM은 빠르게 컴파일하는 C1과 공격적으로 최적화하는 C2를 단계적으로 조합하는 계층형 컴파일(tiered compilation)을 기본으로 사용한다.

JIT는 실행 중 수집한 프로파일을 바탕으로 메서드 인라이닝, 탈출 분석(escape analysis)을 통한 힙 할당 제거, 가상 호출의 단형화 같은 최적화를 수행한다. 가정이 깨지면 다시 인터프리터 실행으로 되돌리는 역최적화(deoptimization)도 가능하다. 이런 구조 덕분에 자바는 이식성과 실행 성능을 함께 얻는다. 참고로 GraalVM 네이티브 이미지처럼 실행 전에 기계어를 만들어 두는 AOT 컴파일(Ahead-Of-Time Compilation) 방식도 있으며, 시작 시간이 중요한 환경에서 활용된다.

단계별 정리

단계수행 주체입력과 출력
컴파일javac.java 소스를 .class 바이트코드로 변환
로딩클래스 로더.class를 읽어 메타스페이스에 클래스 정보 적재
링킹JVM검증, static 필드 준비, 심벌릭 참조 해석
초기화JVMstatic 블록 실행, 최초 사용 시 한 번
실행인터프리터와 JIT바이트코드 해석 실행, 핫스팟은 기계어로 컴파일

면접에서 이렇게 물어본다

  • .java 파일이 실행되기까지의 전체 과정을 단계별로 설명해 보라.
  • 자바가 바이트코드라는 중간 표현을 두는 이유와 그로 인한 장단점은 무엇인가.
  • 클래스 로딩의 로딩, 링킹, 초기화 단계에서 각각 어떤 일이 일어나는가.
  • 인터프리터 방식과 JIT 컴파일 방식이 어떻게 협력하는지, 계층형 컴파일이 무엇인지 설명해 보라.
  • 클래스 로더의 부모 위임 모델이 왜 필요한지 설명해 보라.
Java 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기