자바 컴파일 과정과 JVM 메모리 구조 그리고 GC과정

김코·2026년 1월 29일

자바 컴파일 ~ 실행 과정까지

빌드 타임

  1. 개발자가 소스 코드를 작성하고 이 파일의 확장자는 .java이며, 이는 읽을 수 있는 텍스트 형식이다.
  2. JDK에 포함된 자바 컴파일러(javac)가 소스 코드를 분석한다. 이때 컴퓨터가 바로 이해하는 기계어로가 아닌, 자바 바이트코드로 변환한다. 이때 확장자는 .class이다. → 이때 바이트 코드는 특정 OS에 종속되지 않는 중간 단계 코드

런타임

  1. Java Virtual Machine의 클래스 로더가 실행에 필요한 .class 여러 파일을 찾아 JVM 메모리인 런타임 데이터 영역에 올린다. 이때 자바는 한꺼번에 모든 class 파일을 올리는 게 아닌, 필요한 시점에 동적으로 올린다.
  2. 메모리에 로드된 .class 파일 즉, 바이트코드는 실행 엔진에 의해 기계어로 해석된다. 이때 두 가지 방식이 병행된다.
    1. 인터프리터 : 바이트코드를 한 줄씩 읽어 실행한다. 초기에는 빠르지만, 반복되는 코드도 매번 해석해 전체적인 속도는 느릴 수 있다
    2. Just-In-Time Compiler (JIT 컴파일러) : 위의 인터프리터 단점을 보완한다. 자주 실행되는 코드(= Hot Spot)를 찾아내어 통째로 기계어로 컴파일 한 후 code cache라는 특수 메모리 영역에 저장한다. 이후에는 따로 해석 없이 바로 실행이 가능하다
  3. 프로그램 실행에서 더 이상 사용되지 않는 객체를 알아서 식별해 해제한다. 이에 메모리 관리에 신경을 덜 쓸 수 있다.

과정 요약

소스 코드 작성 → 자바 컴파일러의 변환 → 클래스 로더의 동적 로딩 → 실행 엔진 가동 → 가비지 컬렉터 관리


JVM 런타임 데이터 영역에 관하여

Runtime Data Area는 JVM의 메모리 영역으로 자바가 실행하는 동안 데이터를 저장 및 관리한다.

총 5개의 구조로 나눠져 있다.

  • Method Area
    • JVM이 읽어들인 필드 값, 메소드 데이터, 런타임 상수 풀이다
      • 런타임 상수 풀은 int = 100; 과 같은 리터럴 값과 심볼릭 레퍼런스(메서드 이름)라는 이름 정보가 들어가 있다.
      • 메서드가 호출 될 때, JVM이 상수 풀의 이름만 보고 실제 메모리 주소를 찾아내어 주소값을 상수 풀에 덮어 씌우는 것
  • Heap Area
    • 모든 객체와 배열이 저장된다. 즉, 실제 데이터가 들어가 있는 곳이다 (주소는 스택에)
    • 특히 GC의 메인 관리 대상이며, 효율적인 GC를 위해 Young Generation과 Old Generation으로 다시 나뉜다.

  • Stack Area
    • 메소드 호출 할 때 마다 ‘스택 프레임’ 블록이 쌓인다. 여기에는 지역 변수, 매개 변수, 리턴 값 등이 임시 저장되며, 메소드 종료 시 바로 제거된다.
  • PC Register
    • 현재 수행하고 있는 JVM 명령의 주소를 저장한다. CPU 레지스터 개념과 유사하다
  • Native Method Stack
    • 자바 외의 언어로 작성된 네이티브 코드를 실행하기 위한 메모리 공간이다.

Heap Area에 관하여

런타임 데이터 영역의 Heap 영역은 효율적 관리를 위해 Young Generation과 Old Generation으로 나눈다.

  • Young: 새로운 객체들이 할당된다. 대부분 객체는 여기서 생성되고 사라진다

    • Eden: new 연산자로 생성된 객체가 위치한다
    • Survivor0 , Survivor1 : Eden 영역에 데이터가 가득차면 Eden에 있던 객체가 Survivor0 또는 Survivor1로 옮겨진다.
      옮겨진 객체들은 참조되고 있는 객체들이기에, 둘 중 하나의 영역이 가득차면 다른 Survivor로 이동한다.
      그렇기에 둘 중 하나는 비어 있어야 하는 특징이 있다.
    • 이 과정에서 Minor GC가 발생한다.
      Young 영역에서 발생하는 GC로 Eden 영역 또는 Survivor0, Survivor1에서 사용되지 않는 객체들을 삭제한다.
  • Old: Survivor0, Survivor1을 왔다 갔다 하는 과정에서 살아남은 객체들(즉, Young 영역에서 살아남은 객체들)은 Old 영역으로 이동한다.
    Young 영역보다 크기가 크게 할당되기에 그만큼 GC가 적게 발생한다.
    그럼에도 GC가 발생하게 되는데 Old 영역에서 Major GC(Full GC) 가 발생한다.

  • 번외: Java7까지는 Permanaget Gen 영역이 힙에 포함되었으나, 8부터는 Metaspace라는 이름으로 분리되어 Native Memory 영역으로 옮겨갔다.

그렇다면 GC과정은 어떻게 될까?

GC는 Mark and Sweep 알고리즘 전략을 따른다.

GC가 힙 내부를 돌면서 사용 중인 객체와 사용하지 않는 객체를 식별(Mark)하고 사용되지 않는 객체들을 메모리에서 제거(Sweep)한다.

위 Young 영역에서 Eden에 데이터가 가득차면 Survivor로 옮겨지고 Survivor 1, 2를 왔다 갔다 한다고 했다.
이때 각 객체에는 age bit가 있고 이 과정을 반복하며 객체들이 번갈아 이동할 때 age를 1씩 먹는다.

객체의 age 값이 설정값인 MaxTenuringThreshold 를 초과하면 Old 영역으로 객체를 이동시킨다.

Old 영역의 공간이 부족해지면 Major GC(Full GC)가 발생하고,
이때 Stop-The-World 현상이 발생하는데, 이는 GC를 실행하는 스레드를 제외한 모든 애플리케이션 스레드가 멈춘다.

Major GC는 위에서의 Minor GC보다 훨씬 오래 걸린다. 이 시간을 줄여야하는데 이것이 GC 튜닝의 핵심이다.
힙 영역이 크면 GC 한 번에 애플리케이션 중지되는 시간(STW) 이 길어지고, 너무 작으면 GC가 자주 발생하기에 성능이 떨어지기 때문이다.

Reference

https://velog.io/@ddangle/Java-%EB%9F%B0%ED%83%80%EC%9E%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%98%81%EC%97%ADRuntime-Data-Area%EC%97%90-%EB%8C%80%ED%95%B4

https://blog.devgenius.io/java-virtual-machine-architecture-9009d864fc72

http://equj65.net/tech/java8hotspot/

https://1-7171771.tistory.com/140

profile
백엔드 공부하는 코린이입니다

0개의 댓글