최근 면접에서 Java가 어떻게 실행되는지 컴파일 과정을 질문 받았다.
나는 "Java 가 실행될 때
.java
파일이.class
로 번역되고 JVM 의 클래스로더가 실행시킨다."
정도로 답했지만, 솔직히 말하면 답변을 하면서도 내 설명이 부족하다는 걸 느꼈다. 그리고 꼬리 질문에는 대답할 수 없었다.
그래서 이번 글에서는 JVM(Java Virtual Machine) 의 구조와
Java 프로그램이 실행될 때 내부에서 어떤 일이 벌어지는지를 정리해보려고 한다.
Java를 배우면서 한 번쯤 들어본 개념일 것이다.
JVM은 환경(OS)에 의존하지 않고 Java 프로그램을 어디서든 실행할 수 있게 해준다.
또한, JVM은 메모리 관리 자동화, 보안성 강화, 성능 최적화 같은 역할을 수행한다.
요약하면, JVM은 Java 프로그램이 다양한 환경에서도 안정적이고 빠르게 동작할 수 있게 해주는 핵심 요소다.
이 덕분에 Java는 대규모 서버, 모바일, 클라우드, IoT까지 다양한 분야에서 널리 사용되고 있다.
우리가 작성하는 Java 코드는 컴퓨터가 바로 이해할 수 없다.
바로 실행할 수 없는 Java 코드는 JVM을 통해 바이트코드 형태로 변환되어 실행된다.
Java와 JVM은 이처럼 밀접한 관계를 가진다.
.class
파일 = 바이트코드가 담긴 파일JVM(Java Virtual Machine)은 Java 프로그램을 실행하기 위한 가상 머신이다.
개발자가 작성한 Java 코드를 컴파일한 .class 파일(바이트코드)을 읽어 들여,
운영체제와 하드웨어 환경에 맞게 해석하거나 기계어로 변환해 실행한다.
JVM은 단순히 코드를 실행하는 것 이상의 다양한 기능을 제공한다
다음은 자바 프로그램의 실행 단계이다.
JVM은 크게 다음과 같은 주요 구성요소로 이루어져 있다
클래스 로더 시스템은 .class 파일로 변환된 바이트코드를 JVM 내부로 로드하는 역할을 한다.
필요할 때(on-demand) 클래스를 메모리에 적재하며, 다음 과정을 거친다
클래스 로더는 프로그램이 시작할 때 모든 클래스를 한꺼번에 로드하지 않는다.
대신, 필요할 때 해당 클래스를 찾아 메모리에 적재하는 지연 로딩(Lazy Loading) 방식을 사용한다.
이를 통해 초기 메모리 사용량을 줄이고, 실제로 사용되지 않는 클래스는 끝까지 로드하지 않아 불필요한 메모리 낭비를 방지할 수 있다.
특히 대규모 애플리케이션에서는 이 방식 덕분에 애플리케이션 시작 속도와 메모리 효율성을 크게 높일 수 있다.
실행 엔진은 JVM에 로드된 바이트코드를 실제로 해석하고 실행하는 역할을 한다.
주요 구성요소는 다음과 같다
실행 엔진은 프로그램을 실행할 뿐 아니라, 메모리까지 최적화 관리하는 핵심 역할을 담당한다.
런타임 데이터 영역은 JVM이 프로그램 실행 중 사용하는 메모리 공간을 의미한다.
구체적인 영역은 다음과 같다
각 메모리 영역은 서로 다른 데이터와 동작 방식을 가지며, 프로그램 실행의 기반을 이룬다.
네이티브 메서드 인터페이스는 Java 코드가 JVM 외부의 네이티브 라이브러리(C/C++ 등) 코드를 호출할 수 있게 해주는 기능이다.
JNI를 통해 Java는 플랫폼 독립성을 유지하면서도, 필요한 경우 운영체제나 하드웨어 기능을 사용할 수 있다.
1️⃣ 클래스 로딩(Loading)
.class
파일(바이트코드)을 JVM 메모리로 읽어들인다.(클래스 로더가 담당)2️⃣ 검증(Verification)
3️⃣ 준비(Preparation)
4️⃣ 해석(Resolution)
5️⃣ 실행(Execution)
JVM은 프로그램을 실행할 때 두 가지 방식을 사용한다. 인터프리터 방식과 JIT(Just-In-Time) 컴파일러 방식이다.
현대 JVM은 이 둘을 적절히 조합하여 실행 성능을 최적화한다.
인터프리터는 바이트코드를 한 줄씩 읽어 해석하고 바로 실행하는 방식이다.
JIT 컴파일러는 프로그램을 실행하는 도중, 자주 호출되는 코드(HotSpot)를 찾아내어 바이트코드를 기계어로 변환한다.
변환된 기계어는 코드 캐시(Code Cache) 에 저장되고, 이후부터는 해석 없이 바로 실행할 수 있다.
현대 JVM 은 인터프리터와 JIT 컴파일러를 조합하여 사용한다.
JVM은 프로그램 실행 중 필요한 메모리를 스스로 관리한다.
프로그래머가 직접 메모리를 할당하거나 해제하지 않아도, JVM이 메모리 생성과 회수를 자동으로 처리한다.
가장 핵심적인 역할을 하는 것이 바로 가비지 컬렉션(Garbage Collection, GC) 이다.
가비지 컬렉션은 더 이상 사용되지 않는 객체를 탐지하여 자동으로 메모리에서 제거하는 기능이다.
Java에서는 new 키워드로 생성한 객체들이 힙(Heap) 메모리에 저장되는데,
이 중 참조가 끊긴 객체는 가비지 컬렉션의 대상이 된다.
가비지 컬렉션의 기본 흐름은 다음과 같다
이이번 주제를 정리하게 된 건, 면접 질문 중 "Java 프로그램은 어떻게 실행되나요?" 라는 질문에 막연히 "JVM이 실행시킨다"는 수준으로만 답했던 아쉬움 때문이었다.
JVM은 단순히 바이트코드를 해석해 실행하는 역할에 그치지 않고, 메모리 관리, 성능 최적화(JIT 컴파일러 활용) 등 다양한 기능을 통해 프로그램의 안정성과 실행 효율을 함께 책임진다.
JVM 내부에서 어떤 일이 벌어지는지 이해하면, Java 애플리케이션의 성능을 보다 체계적으로 분석하고 최적화할 수 있는 기반을 마련할 수 있다.
다음 글에서는
이 내용에 대해서 조금 더 구체적으로 살펴볼 예정이다.