TIL/Java

자바 컴파일 과정 | .java 파일은 어떻게 실행될까?

beady 2026. 3. 26. 22:25

자바를 처음 공부할 때 자주 듣는 말 중 하나가 있다.
바로 “자바는 운영체제에 독립적이다.” 라는 말이다.

 

그렇다면 자바는 어떻게 Windows, Linux, macOS처럼 서로 다른 환경에서 같은 코드를 실행할 수 있을까?

이 질문의 답은 자바 컴파일 과정JVM(Java Virtual Machine)에 있다.


이번 글에서는 .java 파일이 실제로 어떤 과정을 거쳐 실행되는지 흐름을 살펴보며 정리해보려고 한다.


1. 자바 실행의 전체 흐름

자바 프로그램의 실행 흐름을 먼저 크게 보면 다음과 같다.

 

 

단계별로 살펴보자면,

 

1. 개발자가 자바 소스 코드(.java)를 작성한다.

2. javac(자바 컴파일러)가 자바 소스 코드를 바이트코드(.class)로 컴파일한다.
3. JVM이 바이트코드를 클래스 로딩한 뒤, 인터프리터로 해석해서 실행하거나 JIT 컴파일러를 통해 기계어로 변환 후 실행한다.

 

즉, 자바는 소스 코드를 바로 기계어로 실행하는 방식이 아니라, 중간에 JVM이 이해할 수 있는 형태의 코드를 한 번 거쳐서 실행하는 구조라고 볼 수 있다.


2. .java 파일은 어떻게 .class 파일이 될까?

예를 들어 아래와 같은 자바 코드가 있다고 하자.

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello Java");
    }
}

 

이 파일을 Hello.java라는 이름으로 저장한 뒤 아래 명령어를 실행하면,

javac Hello.java

 

컴파일 결과로 Hello.class 파일이 생성된다.

 

여기서 중요한 점은 Hello.class가 CPU가 바로 이해하는 기계어가 아니라는 것이다.
이 파일은 JVM이 이해할 수 있는 바이트코드(Bytecode) 이다.

 

즉, 자바는 소스 코드를 곧바로 운영체제별 실행 파일로 만드는 것이 아니라, 먼저 플랫폼 중립적인 중간 코드로 변환한다.


3. 바이트코드는 왜 중요한가?

자바가 운영체제 독립성을 가질 수 있는 이유가 바로 이 바이트코드에 있다.

일반적으로 어떤 언어는 소스 코드를 특정 운영체제와 CPU 환경에 맞는 기계어로 바로 컴파일한다.

C, C++, Go와 같은 언어는 소스 코드를 특정 운영체제와 CPU 환경에 맞는 기계어로 직접 컴파일하기 때문에 플랫폼에 종속적인 특징을 가진다.

이 경우 운영체제가 달라지면 다시 컴파일해야 할 수도 있다.

 

하지만 자바는 다르다.

자바는 소스 코드를 먼저 공통 바이트코드로 만들고, 그 바이트코드를 각 운영체제에 맞는 JVM이 실행한다.

즉,

  • Windows에서는 Windows용 JVM이 실행한다.
  • Linux에서는 Linux용 JVM이 실행한다.
  • macOS에서는 macOS용 JVM이 실행한다.

입력은 같은 .class 파일이지만, 각 환경의 JVM이 이를 해석하고 실행하기 때문에 자바는 대표적인 플랫폼 독립 언어로 불린다.


4. JVM은 바이트코드를 어떻게 실행할까?

.class 파일이 만들어졌다고 해서 바로 실행되는 것은 아니다.
JVM은 이 바이트코드를 실행하기 전에 몇 가지 과정을 거친다.

 

4-1. 클래스 로딩(Class Loading)

먼저 JVM의 Class Loader가 필요한 클래스 파일을 메모리에 적재한다.

 

자바는 프로그램 시작 시 모든 클래스를 한 번에 올리는 것이 아니라,

필요한 시점에 필요한 클래스만 불러올 수 있다.


이런 방식을 동적 로딩(Dynamic Loading) 이라고 한다.

즉, JVM은 실행에 필요한 클래스들을 순차적으로 메모리에 올리면서 프로그램을 구성해 나간다.

 

4-2. 링크 과정(Linking)

클래스를 메모리에 올린 뒤에는 실행 전에 연결 작업을 수행한다.
이 과정을 보통 링크(Linking)라고 한다.

링크 과정은 다음 세 단계로 구성된다.

1) 검증(Verification)

바이트코드가 JVM 명세에 맞게 작성되었는지 검사한다.
잘못된 구조이거나 보안상 문제가 있는 코드가 실행되지 않도록 확인하는 단계이다.

2) 준비(Preparation)

클래스 변수(static 변수)를 위한 메모리를 할당하고 기본값으로 초기화한다.

예를 들면 다음과 같다.

  • int → 0
  • boolean → false
  • 참조형 → null

3) 해결(Resolution)

클래스, 메서드, 필드 등에 대한 참조를 실제 사용할 수 있는 형태로 변환한다.

즉, 이름으로만 알고 있던 대상을 실제 메모리 주소나 참조 형태로 연결하는 과정이라고 볼 수 있다.

 

4-3. 초기화(Initialization)

링크 과정이 끝나면 초기화 단계가 진행된다.

이 단계에서는 클래스에 선언된 static 변수에 실제 값이 할당되고, static 블록도 함께 실행된다.

 

예를 들어 아래 코드가 있다면,

class Test {
    static int num = 10;

    static {
        System.out.println("static block");
    }
}

 

초기화 단계에서 num에 10이 들어가고, static 블록이 실행된다.

 

즉, 초기화는 단순히 메모리만 확보하는 단계가 아니라,

클래스를 실제 실행 가능한 상태로 완성하는 마지막 단계라고 볼 수 있다.


5. 실행 단계 : JVM은 바이트코드를 어떻게 실행할까?

클래스 로딩, 링크, 초기화 과정이 끝나면 이제 실제로 바이트코드가 실행되는 단계로 넘어간다.
이때 JVM은 Execution Engine(실행 엔진)을 통해 바이트코드를 실행한다.

여기서 자바는 대표적으로 두 가지 방식을 사용한다.

 

5-1. 인터프리터 방식

인터프리터는 바이트코드를 한 줄씩 읽고 해석하면서 실행한다.

장점은 바로 실행할 수 있다는 점이지만,

같은 코드가 반복 실행될 때마다 계속 해석해야 하므로 성능상 비효율이 생길 수 있다.

 

5-2. JIT 컴파일러 방식

이런 단점을 보완하기 위해 등장한 것이 JIT(Just-In-Time) Compiler 이다.

JIT 컴파일러는 자주 실행되는 바이트코드를 기계어로 변환해 두었다가,

이후에는 해석 과정을 반복하지 않고 더 빠르게 실행할 수 있도록 돕는다.

즉, 자바는 인터프리터만 사용하는 것이 아니라 인터프리터와 JIT을 함께 사용하면서 실행 효율을 높인다.

그래서 예전에는 자바가 느리다는 인식이 강했지만, 실제로는 JIT 덕분에 반복 실행 환경에서 꽤 좋은 성능을 낼 수 있다.


6. C 언어와 비교하여 이해하기

자바 컴파일 과정을 이해할 때는 C 언어와 비교하면 더 직관적이다.

C는 보통 소스 코드를 바로 운영체제와 CPU가 이해할 수 있는 기계어로 컴파일한다.
즉, 실행 파일이 특정 환경에 종속되기 쉽다.

자바와 비교하면 다음과 같은 구조를 가진다.

  • C : 소스 코드 → 기계어
  • Java : 소스 코드 → 바이트코드 →  해당 운영체제에 맞는 JVM이 실행

즉, 자바는 실행을 JVM에게 맡기는 구조이기 때문에 운영체제가 달라도 같은 바이트코드를 사용할 수 있다.

 


7. 정리

결국 자바의 핵심은 “소스 코드를 한 번 바이트코드로 바꾸고, 각 운영체제의 JVM이 이를 실행한다” 는 점이다.

이 구조 덕분에 자바는 운영체제에 독립적인 언어라는 특징을 가지게 된다.

 

자바 컴파일 과정은 단순히 .java 파일이 .class 파일로 바뀌는 과정만 의미하지 않는다.
실제로는 JVM이 클래스를 로딩하고, 검증하고, 초기화하고, 실행하는 전체 흐름까지 함께 이해해야 더 정확하게 볼 수 있다.

 

자바를 공부할수록 결국 JVM 이해가 굉장히 중요하다는 걸 느끼게 되는데,
컴파일 과정은 그 시작점으로 보기 좋은 주제인 것 같다.