코드 한 줄의 기록

Java 컴파일과 실행 과정 완벽 가이드: JVM 작동 원리 이해하기 본문

JAVA

Java 컴파일과 실행 과정 완벽 가이드: JVM 작동 원리 이해하기

CodeByJin 2025. 9. 19. 10:36
반응형

Java는 “Write Once, Run Anywhere”를 실현한 언어로, 소스 코드를 컴파일하고 JVM이 실행하는 독특한 구조를 갖추고 있습니다. 이 글에서 Java 컴파일러와 JVM의 내부 동작 과정을 단계별로 살펴보고, 개발자가 이해해야 할 핵심 개념과 팁을 자연스럽고 구어체에 가깝게 알려드립니다.

왜 컴파일과 JVM 동작 과정을 알아야 할까?

  • 디버깅과 성능 최적화
    • 버그가 발생하거나 성능 저하 문제를 마주했을 때, 컴파일러 옵션이나 JVM 설정을 조정하면 문제 해결에 효과적입니다.
  • 플랫폼 독립성 이해
    • Java의 핵심 강점 중 하나인 플랫폼 독립성(Portability)은 바이트코드(.class 파일)와 JVM 덕분에 나옵니다.
  • 최신 Java 기능 활용
    • 모듈 시스템, JIT 컴파일러, GC(Garbage Collector) 등 최신 Java 기능의 동작 원리를 알면 프로젝트에 맞는 JVM 튜닝과 코드를 작성할 수 있습니다.

Java 컴파일 단계: 소스에서 바이트코드까지

1. 자바 소스(.java) 파일

개발자가 작성하는 .java 파일에는 클래스, 인터페이스, 메소드, 필드가 정의되어 있습니다.
- 패키지 선언: package com.example;
- 클래스 선언: public class MyApp { … }


2. javac 컴파일러

터미널에서 javac MyApp.java 명령을 실행하면, 자바 컴파일러가 다음 작업을 수행합니다.

  1. 렉시컬 분석(Lexical Analysis)
    소스 코드를 토큰(token)으로 분리
  2. 구문 분석(Syntax Analysis)
    토큰 스트림을 AST(Abstract Syntax Tree)로 변환
  3. 의미 분석(Semantic Analysis)
    타입 검사, 참조 유효성 확인
  4. 바이트코드 생성(Bytecode Generation)
    AST를 기반으로 .class 파일에 JVM 명령어 형태로 변환

: 컴파일 오류가 나면, 오류 메시지의 위치와 내용을 꼼꼼히 확인하고, 빈번한 실수인 세미콜론(;) 빠짐, 타입 불일치 등을 우선 점검하세요.

JVM 내부 구조와 실행 흐름

JVM은 자바 바이트코드를 로딩(load)하고, 해석(interpret)하거나 JIT(Just-In-Time) 컴파일하여 머신 코드로 변환한 뒤 실행합니다. JVM은 크게 클래스로더, 메모리 영역, 실행 엔진으로 구성됩니다.

1. 클래스 로더(Class Loader)

  • Bootstrap ClassLoader: JDK 내부 핵심 클래스(rt.jar) 로드
  • Extension ClassLoader: JRE 확장 라이브러리 로드
  • Application ClassLoader: 애플리케이션 클래스패스에 있는 클래스 로드

클래스 로더는 계층 구조로 위에서 아래로 클래스를 찾으며, 네이티브 라이브러리(.dll, .so) 로딩도 담당합니다.

2. JVM 메모리 구조

  1. 메소드(Method) 영역
    • 클래스 구조, 상수 풀, 메소드 바이트코드 저장
  2. 힙(Heap)
    • 인스턴스 객체와 배열 저장, 가비지 컬렉션 대상
  3. 스택(Stack)
    • 각 쓰레드별로 호출 스택(Stack Frame) 생성
    • 지역 변수, 피연산자 스택, 프레임 데이터 포함
  4. 프로그램 카운터(PC) 레지스터
    • 현재 실행 중인 바이트코드 위치 저장
  5. 네이티브 메소드 스택(Native Method Stack)
    • JNI(Java Native Interface) 호출 시 사용

3. 실행 엔진(Execution Engine)

  • 인터프리터: 바이트코드를 한 줄씩 해석
  • JIT 컴파일러: 자주 사용하는 바이트코드를 머신 코드로 변환 캐싱
  • 가비지 컬렉터(GC): 힙의 사용되지 않는 객체 메모리 해제

: 대규모 서비스에서는 GC 튜닝이 핵심입니다. G1, ZGC 등 최신 GC 알고리즘을 프로젝트 특성에 맞게 선택하세요.

실제 실행 흐름: 예시로 살펴보기

# 프로젝트 구조
src/

 └─ com/example/MyApp.java

# 컴파일
javac -d out src/com/example/MyApp.java

# 실행
java -cp out com.example.MyApp
  1. javacout/com/example/MyApp.class 생성
  2. java 명령이 Application ClassLoader에 의해 클래스 로드
  3. main() 메소드가 호출되어 Stack Frame 생성
  4. 바이트코드가 해석 또는 JIT 컴파일 후 실행
  5. main() 종료 → Stack Frame 제거
  6. 프로그램 종료 후 GC 정리, JVM 종료

JVM 옵션과 성능 튜닝

  • -Xms, -Xmx: 초기/최대 힙 크기 설정
  • -XX:+UseG1GC: G1 GC 활성화
  • -XX:MetaspaceSize, -XX:MaxMetaspaceSize: 메타스페이스 크기 조정
  • -XX:+PrintGCDetails: GC 로그 출력

성능 팁
- 로컬 개발 환경에서 GC 로그를 분석하고, 메모리 사용 패턴에 따라 힙 크기를 조절하세요.
- 클라우드 환경에서는 컨테이너 메모리 한계와 JVM 힙 크기를 조율하는 것이 안정적 운영의 핵심입니다.

Java 9 이후의 모듈 시스템과 JVM

Java 9부터 도입된 모듈 시스템(Jigsaw)은 module-info.java로 모듈 간 의존성을 명시합니다.
장점: 모듈 별 접근 제어, 경량화된 런타임 이미지 생성(jlink)

사용법

// module-info.java
module com.example.myapp {
    requires java.sql;
    exports com.example.service;
}

실행: java --module-path mods -m com.example.myapp/com.example.Main

모듈 시스템은 대규모 프로젝트에서 클래스 충돌과 의존성 관리를 단순화해 줍니다.

정리 및 실전 팁

Java 컴파일과 JVM 실행 과정은 바이트코드, 클래스로더, 메모리 구조, 실행 엔진 네 부분으로 요약할 수 있습니다.

실무에서 바로 활용할 수 있는 팁:
- 컴파일 최적화: javac 옵션에 -g:none을 추가해 디버그 정보를 제외하면 클래스파일 크기 감소
- JIT 힌트: -XX:CompileThreshold로 JIT 컴파일 기준 변경
- GC 튜닝: 프로덕션 환경에서 적절한 GC 알고리즘 선택과 힙 크기 조절


이 글을 참고해 Java 개발과 운영 과정에서 발생하는 오류와 성능 이슈를 스스로 분석·해결할 수 있길 바랍니다. 여러분의 프로젝트에 Java의 진정한 가치를 더해 보세요!

반응형