본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
16장 : JVM과 진단

클래스 로딩, 바이트코드와 JIT

Java 소스는 바이트코드로 컴파일되고 JVM은 필요한 클래스를 로드해 검증하고 연결하고 초기화합니다.

클래스 로더는 이름이 같은 클래스도 로더가 다르면 서로 다른 타입으로 취급할 수 있습니다.

인터프리터는 코드를 바로 실행하고 JIT 컴파일러는 자주 실행되는 경로를 관찰해 네이티브 코드로 최적화합니다.

워밍업 전후 성능이 달라질 수 있는 이유입니다.

이 절에서는 빌드 산출물과 실행 중 최적화를 분리합니다.

JVM에서 타입의 정체성은 전체 클래스 이름만이 아니라 그 클래스를 정의한 로더까지 포함합니다. 플러그인처럼 격리된 로더 둘이 같은 class 파일을 읽으면 이름과 바이트가 같아도 캐스팅할 수 없는 별도 타입이 됩니다. 클래스 로딩 문제를 조사할 때 로더 정보가 필요한 이유입니다.


로딩은 클래스 바이트를 Class 객체로 만든다

부트스트랩, 플랫폼, 애플리케이션 로더가 위임 관계로 클래스를 찾습니다.

classpath와 module path가 탐색 입력이 됩니다.


검증과 연결이 바이트코드 안전성을 확인한다

형식과 타입 규칙을 검사하고 정적 필드 공간과 심볼 참조를 준비합니다.

일부 참조 해결은 실제 사용 시점까지 지연될 수 있습니다.

JvmExecution.java는 정적 초기화, 실제 클래스 로더, 반복 호출을 한 실행에 배치해 서로 다른 JVM 단계를 관찰하게 합니다.

JvmExecution.java
public class JvmExecution {
    static final String VALUE;
    static {
        VALUE = "initialized";
        System.out.println("static init");
    }

    static int hotMethod(int value) { return value * 2; }

    public static void main(String[] args) {
        ClassLoader loader = JvmExecution.class.getClassLoader();
        System.out.println(loader.getClass().getName());
        System.out.println(VALUE);
        int result = 0;
        for (int i = 0; i < 100_000; i++) result = hotMethod(i);
        System.out.println(result);
    }
}

JVM은 main을 호출하기 전에 JvmExecution을 초기화하므로 static init이 가장 먼저 보입니다. 로더 이름은 JDK 구현과 실행 방식에 따라 달라질 수 있고, 마지막 값은 반복의 최종 i인 99,999를 두 배 한 결과입니다.

static init
<애플리케이션 클래스 로더 이름>
initialized
199998

클래스 초기화가 main보다 먼저 한 번 실행되고 hotMethod는 반복 호출되며 JIT가 관찰할 수 있습니다.


초기화가 static 초기화 코드를 실행한다

클래스의 첫 적극적 사용에서 정적 필드 초기화와 static 블록이 소스 순서로 실행됩니다.

초기화 순환을 피합니다.

소스 실행만으로 보이지 않는 단계는 javap와 클래스 로딩 로그를 이용해 관찰할 수 있습니다.

비교 1
javac JvmExecution.java
javap -c -p JvmExecution
java -Xlog:class+load=info JvmExecution
비교 2
// 같은 이름이어도 서로 다른 ClassLoader가 정의하면 다른 타입이다.
// static 초기화는 클래스를 처음 적극적으로 사용할 때 한 번 실행된다.
// 반복 호출은 JIT 프로파일과 최적화 후보가 될 수 있다.

JIT는 관찰한 실행 경로를 최적화한다

호출 빈도와 타입 프로필을 바탕으로 인라이닝과 특수화를 수행하고 가정이 깨지면 최적화를 되돌릴 수 있습니다.

javap -c -p 결과에서 hotMethod 호출 바이트코드를 찾고, -XX:+PrintCompilation을 지원하는 실행 환경에서는 컴파일 로그와 대조해 보세요. 반복 횟수를 바꿨을 때 JIT 컴파일 여부나 시점이 달라질 수 있으며, 소스에 반복문이 있다고 특정 최적화를 보장하는 것은 아닙니다.

JIT는 관찰한 타입과 분기를 바탕으로 공격적으로 인라인한 뒤 가정이 깨지면 deoptimization으로 인터프리터나 덜 최적화된 코드로 돌아갈 수 있습니다. 그래서 짧은 단일 실행 시간을 언어 성능으로 일반화하지 말고 워밍업과 실제 부하를 포함해 측정해야 합니다.

클래스 초기화 실패는 해당 타입을 오류 상태로 만들 수 있습니다. static 블록에서 외부 I/O나 복잡한 의존 초기화를 수행하면 첫 사용 시점의 실패 원인이 멀어지므로, 상수 설정과 필수 불변식 외의 무거운 작업은 명시적인 수명 관리로 옮기는 편이 좋습니다.


JVM 실행은 class 파일을 한 번 번역하는 과정이 아니라 로딩과 검증, 초기화, 프로필 기반 최적화가 이어지는 과정입니다.