[Java][JVM] Java Virtual Machine의 구조와 동작 원리

이영재·2025년 5월 2일
0

JVM

목록 보기
2/5

0. 들어가며

최근 면접에서 Java가 어떻게 실행되는지 컴파일 과정을 질문 받았다.

나는 "Java 가 실행될 때 .java 파일이 .class 로 번역되고 JVM 의 클래스로더가 실행시킨다."

정도로 답했지만, 솔직히 말하면 답변을 하면서도 내 설명이 부족하다는 걸 느꼈다. 그리고 꼬리 질문에는 대답할 수 없었다.

  • 클래스 로더는 정확히 어떤 역할을 하나요?
  • 인터프리터와 JIT 컴파일러의 차이는 뭔가요?
  • JVM의 가비지 컬렉션(GC) 동작 방식은?

그래서 이번 글에서는 JVM(Java Virtual Machine) 의 구조와
Java 프로그램이 실행될 때 내부에서 어떤 일이 벌어지는지를 정리해보려고 한다.

1. 서론

1.1 JVM이 왜 필요한가

Java를 배우면서 한 번쯤 들어본 개념일 것이다.
JVM은 환경(OS)에 의존하지 않고 Java 프로그램을 어디서든 실행할 수 있게 해준다.
또한, JVM은 메모리 관리 자동화, 보안성 강화, 성능 최적화 같은 역할을 수행한다.

요약하면, JVM은 Java 프로그램이 다양한 환경에서도 안정적이고 빠르게 동작할 수 있게 해주는 핵심 요소다.
이 덕분에 Java는 대규모 서버, 모바일, 클라우드, IoT까지 다양한 분야에서 널리 사용되고 있다.

1.2 Java와 JVM의 관계

우리가 작성하는 Java 코드는 컴퓨터가 바로 이해할 수 없다.
바로 실행할 수 없는 Java 코드는 JVM을 통해 바이트코드 형태로 변환되어 실행된다.
Java와 JVM은 이처럼 밀접한 관계를 가진다.

2. JVM이란 무엇인가

2.1 JVM 정의

  • .class 파일 = 바이트코드가 담긴 파일

JVM(Java Virtual Machine)은 Java 프로그램을 실행하기 위한 가상 머신이다.
개발자가 작성한 Java 코드를 컴파일한 .class 파일(바이트코드)을 읽어 들여,
운영체제와 하드웨어 환경에 맞게 해석하거나 기계어로 변환해 실행한다.

2.2 JVM이 제공하는 특징

JVM은 단순히 코드를 실행하는 것 이상의 다양한 기능을 제공한다

  • 플랫폼 독립성 : 운영체제나 하드웨어에 관계없이 동일한 바이트코드를 실행할 수 있다.
  • 메모리 관리 자동화 : 객체의 생성과 소멸을 JVM이 관리하며, 더 이상 필요 없는 객체는 가비지 컬렉터(GC)가 자동으로 제거한다.
  • 보안 강화 : 바이트코드를 검증하고, 외부 코드의 직접 실행을 제한하여 프로그램의 보안을 높인다.
  • 성능 최적화 : 초기에는 인터프리터 방식으로 실행하지만, 자주 실행되는 코드는 JIT 컴파일러가 기계어로 변환해 성능을 향상시킨다.

3. JVM 아키텍처

다음은 자바 프로그램의 실행 단계이다.

JVM은 크게 다음과 같은 주요 구성요소로 이루어져 있다

3.1 클래스 로더 시스템(Class Loader System)

클래스 로더 시스템은 .class 파일로 변환된 바이트코드를 JVM 내부로 로드하는 역할을 한다.
필요할 때(on-demand) 클래스를 메모리에 적재하며, 다음 과정을 거친다

  • 로딩(Loading): 바이트코드를 읽어들인다.
  • 링크(Linking): 참조하는 다른 클래스나 메서드를 연결한다.
  • 초기화(Initialization): static 변수 초기화, static 블록 실행을 처리한다.

    클래스 로더는 프로그램이 시작할 때 모든 클래스를 한꺼번에 로드하지 않는다.
    대신, 필요할 때 해당 클래스를 찾아 메모리에 적재하는 지연 로딩(Lazy Loading) 방식을 사용한다.
    이를 통해 초기 메모리 사용량을 줄이고, 실제로 사용되지 않는 클래스는 끝까지 로드하지 않아 불필요한 메모리 낭비를 방지할 수 있다.
    특히 대규모 애플리케이션에서는 이 방식 덕분에 애플리케이션 시작 속도와 메모리 효율성을 크게 높일 수 있다.

3.2 실행 엔진(Execution Engine)

실행 엔진은 JVM에 로드된 바이트코드를 실제로 해석하고 실행하는 역할을 한다.
주요 구성요소는 다음과 같다

  • 인터프리터(Interpreter): 바이트코드를 한 줄씩 해석하여 실행한다. (초기 실행 빠름, 반복 시 느림)
    • Java 가 컴파일 언어 이면서 인터프리터 언어라고 하는 이유
  • JIT(Just-In-Time) 컴파일러: 자주 호출되는 코드를 기계어로 변환해 성능을 최적화한다.
    • 실행 중에 바이트코드를 분석하여, 반복적으로 호출되는 메서드나 루프를 감지해 기계어로 변환하고, 코드 캐시(Code Cache) 에 저장
  • 가비지 컬렉터(Garbage Collector): Heap 메모리를 관리하며, 더 이상 사용되지 않는 객체를 자동으로 수거한다.

    실행 엔진은 프로그램을 실행할 뿐 아니라, 메모리까지 최적화 관리하는 핵심 역할을 담당한다.

3.3 런타임 데이터 영역(Runtime Data Areas)

런타임 데이터 영역은 JVM이 프로그램 실행 중 사용하는 메모리 공간을 의미한다.
구체적인 영역은 다음과 같다

  • 메서드 영역(Method Area): 클래스 정보(메타데이터), static 변수 등을 저장.
  • 힙(Heap): 인스턴스 객체가 저장되는 공간. 가비지 컬렉터가 관리하는 주요 대상.
  • 스택(Stack): 메서드 호출 시 생성되는 프레임을 저장. 지역 변수, 매개변수 등이 위치.
  • PC 레지스터(Program Counter Register): 현재 실행 중인 명령어 주소를 저장.
  • 네이티브 메서드 스택(Native Method Stack): C, C++ 등 네이티브 메서드 호출 시 사용하는 스택.

    각 메모리 영역은 서로 다른 데이터와 동작 방식을 가지며, 프로그램 실행의 기반을 이룬다.

3.4 네이티브 메서드 인터페이스(Native Method Interface, JNI)

네이티브 메서드 인터페이스는 Java 코드가 JVM 외부의 네이티브 라이브러리(C/C++ 등) 코드를 호출할 수 있게 해주는 기능이다.

  • 이를 통해 성능 최적화, 하드웨어 접근, 레거시 시스템 연동 등이 가능하다.

    JNI를 통해 Java는 플랫폼 독립성을 유지하면서도, 필요한 경우 운영체제나 하드웨어 기능을 사용할 수 있다.

4. Java 프로그램의 실행 흐름

1️⃣ 클래스 로딩(Loading)

  • .class 파일(바이트코드)을 JVM 메모리로 읽어들인다.(클래스 로더가 담당)
  • 이때 "필요할 때(on-demand)" 로딩하는 지연 로딩 방식도 적용된다.

2️⃣ 검증(Verification)

  • JVM은 로드한 클래스 파일이 정상적인 바이트코드인지 검사한다.
  • 악성코드, 규격 위반 코드, 타입 불일치 등을 막기 위해 필수.
  • 실패하면 java.lang.VerifyError 발생.

3️⃣ 준비(Preparation)

  • 메서드 영역에 static 변수를 위한 메모리 공간을 할당한다.
  • 단, 이 시점에서는 값 초기화는 하지 않고 기본값(0, null)만 설정한다.

4️⃣ 해석(Resolution)

  • 클래스나 인터페이스, 메서드, 필드의 참조(이름 → 실제 메모리 주소) 를 연결한다.
  • 예를 들면, System.out.println() 호출 시 System, out, println 각각을 실제 메모리에 매핑하는 작업.

5️⃣ 실행(Execution)

  • 준비와 해석이 끝난 바이트코드를 실제로 실행하는 단계.
  • 여기서 인터프리터(Interpreter) 또는 JIT 컴파일러를 통해 프로그램이 구동된다.

5. JVM의 실행 방식

JVM은 프로그램을 실행할 때 두 가지 방식을 사용한다. 인터프리터 방식과 JIT(Just-In-Time) 컴파일러 방식이다.
현대 JVM은 이 둘을 적절히 조합하여 실행 성능을 최적화한다.

5.1 인터프리터 방식

인터프리터는 바이트코드를 한 줄씩 읽어 해석하고 바로 실행하는 방식이다.

  • 장점은 빠른 시작 속도다. 컴파일 없이 바로 실행할 수 있기 때문에 프로그램 시작이 빠르다.
  • 단점은 반복 실행 성능이 떨어진다는 점이다. 매번 같은 바이트코드를 해석해야 하므로, 시간이 지날수록 성능이 비효율적이 된다.

5.2 JIT 컴파일러 방식

JIT 컴파일러는 프로그램을 실행하는 도중, 자주 호출되는 코드(HotSpot)를 찾아내어 바이트코드를 기계어로 변환한다.
변환된 기계어는 코드 캐시(Code Cache) 에 저장되고, 이후부터는 해석 없이 바로 실행할 수 있다.

  • 장점은 한 번 변환된 코드는 CPU가 직접 실행하므로 성능이 대폭 향상된다.
  • 단점은 바이트코드를 기계어로 변환하는 과정이 있기 때문에 초기에는 시간이 좀 걸린다.

5.3 둘의 조합 (초기 인터프리트, 이후 JIT 최적화)

현대 JVM 은 인터프리터와 JIT 컴파일러를 조합하여 사용한다.

  • 프로그램 시작 초기에는 인터프리터 방식으로 빠르게 실행을 시작한다.
  • 실행하면서 자주 호출되는 메서드나 루프를 프로파일링하고,
  • 일정 기준(예: 호출 횟수 1,500회 이상)이 넘으면 JIT 컴파일러가 개입하여 기계어로 변환한다.

6. JVM의 메모리 관리

JVM은 프로그램 실행 중 필요한 메모리를 스스로 관리한다.
프로그래머가 직접 메모리를 할당하거나 해제하지 않아도, JVM이 메모리 생성과 회수를 자동으로 처리한다.

가장 핵심적인 역할을 하는 것이 바로 가비지 컬렉션(Garbage Collection, GC) 이다.

  • 가비지 컬렉션이 작동하면서 JVM이 메모리 생성과 회수를 자동으로 처리하는 모습

6.1 가비지 컬렉션(Garbage Collection, GC)

가비지 컬렉션은 더 이상 사용되지 않는 객체를 탐지하여 자동으로 메모리에서 제거하는 기능이다.

Java에서는 new 키워드로 생성한 객체들이 힙(Heap) 메모리에 저장되는데,
이 중 참조가 끊긴 객체는 가비지 컬렉션의 대상이 된다.

가비지 컬렉션의 기본 흐름은 다음과 같다

  • JVM은 메모리 상황을 주기적으로 감시한다.
  • 사용되지 않는 객체(더 이상 참조되지 않는 객체)를 식별한다.
  • 해당 객체를 제거하여 메모리 공간을 회수한다.
  • 이 과정을 통해 메모리 누수(Memory Leak) 를 방지하고,
  • Java 프로그램이 안정적으로 장시간 실행될 수 있도록 돕는다.

7. 결론

이이번 주제를 정리하게 된 건, 면접 질문 중 "Java 프로그램은 어떻게 실행되나요?" 라는 질문에 막연히 "JVM이 실행시킨다"는 수준으로만 답했던 아쉬움 때문이었다.

JVM은 단순히 바이트코드를 해석해 실행하는 역할에 그치지 않고, 메모리 관리, 성능 최적화(JIT 컴파일러 활용) 등 다양한 기능을 통해 프로그램의 안정성과 실행 효율을 함께 책임진다.

JVM 내부에서 어떤 일이 벌어지는지 이해하면, Java 애플리케이션의 성능을 보다 체계적으로 분석하고 최적화할 수 있는 기반을 마련할 수 있다.

다음 글에서는

  • JIT 컴파일러의 동작 원리
  • JVM 메모리 관리(Garbage Collection 등)

이 내용에 대해서 조금 더 구체적으로 살펴볼 예정이다.

0개의 댓글