[Java] 1. JVM과 자바 코드 실행 과정

1. JVM이란?

1.1. JDK, JRE 그리고 JVM

JDK

- Java Development Kit

- 자바 언어를 사용하여 소프트웨어를 개발하기 위한 개발 키트

- 자바 프로그래밍을 하기 위해 Oracle 사이트에서 다운받는 그! 파일

- 컴파일러인 javac, JRE, 개발 도구 및 라이브러리 등이 포함되어 있음

 

JRE

- Java Runtime Environment

- 자바 애플리케이션을 실행하는 환경을 제공함

- 컴파일러와 개발 도구를 포함하지 않으며, 순수하게 자바 어플리케이션을 실행하는 데 필요한 라이브러리와 실행 환경만 포함함

JVM

- Java Virtual Machine

- 자바 바이트 코드를 실제로 실행하는 가상 머신

 

1.2. JVM의 특징

플랫폼 독립성

- 자바 코드는 컴파일된 바이트 코드로 변환되고, JVM은 이 바이트 코드를 특정 운영체제나 하드웨어에 종속되지 않고 실행함

- 이로서 자바 프로그램은 여러 플랫폼에서 동일한 방식으로 실행될 수 있음

 

자동 메모리 관리

- JVM은 가비지 컬랙션(Garbage Collection)을 통해 메모리를 자동으로 관리함

- 개발자가 명시적으로 메모리를 할당하거나 해제할 필요가 없어 편리함

 

보안 기능

- JVM은 자바 애플리케이션을 보호하기 위한 다양한 보안 기능을 제공함

- 클래스 로딩, 코드 검증, 암호화, 권한 관리 등이 포함됨

 

 

1.3. JVM의 주요 구성 요소

ClassLoader (클래스 로더)

- JVM은 클래스 로더를 통해 자바 클래스 파일을 로드함

- 클래스 로더는 필요한 클래스를 찾아서 메모리에 로딩하며, 클래스의 인스턴스를 생성하고, 클래스의 정적 멤버(메서드와 변수)에 접근할 수 있도록 함

 

Execution Engine (실행 엔진)

- 클래스가 메모리에 로드되면, 실행 엔진이 해당 클래스의 바이트 코드를 해석하고 실행함

- JVM은 인터프리터와 JIT 컴파일러를 함께 사용하여 실행 속도를 최적화함

 

Memory Area (메모리 영역)

- JVM은 프로그램 실행 중에 메모리를 관리함

- 메모리 영역은 크게 다음과 같이 나뉨

 

Method Area

- 클래스 정보와 클래스 변수(static 변수)를 저장하는 공간

Heap

- 객체와 배열이 할당되는 공간으로, 가비지 컬렉션을 통해 메모리 관리를 수행함

Stack

- 메서드 호출과 관련된 로컬 변수, 연산 중간 결과, 메서드 호출 스택을 저장함

PC Register

- 현재 실행 중인 스레드의 위치를 저장함

Native Method Method

- 자바 외부에서 로출되는 네이티브 메서드의 스택을 저장함

 

Garbage Collection (가비지 컬렉션)

- JVM은 가비지 컬렉션을 통해 더 이상 참조되지 않는 객체를 자동으로 제거하여 메모리 누수를 방지함

 

Thread Management (스레드 관리)

- JVM은 멀티스레딩을 지원하며, 스레드를 관리하고 스레드 간 동기화를 제어함

- 이를 통해 병렬 프로그래밍을 가능하게 함

 

2. HelloWorld.java를 실행하면?

 컴파일을 통해 바이트 코드를 생성하고, JVM을 통해 바이트 코드를 실행하는 과정을 알아보자

2.1. 소스 코드 작성하기

- 개발자가 VSCode 등의 IDE에서 자바 소스 코드를 작성하고 저장함

- .java 파일이 생성됨

 

2.2. 컴파일하기

- 프로그래밍 언어로 작성된 소스 코드를 컴퓨터가 이해하고 실행할 수 있는 형태로 변환하는 과정

- 자바 소스 코드 파일(.java)을 컴파일해서 바이트 코드 파일(.class)을 생성를 생성함

- 그렇다면 누가, 어떻게 컴파일을 하는 것일까?

 

Step 1. 컴파일러 호출

- IDE는 내부적으로 JDK에 포함된 javac 컴파일러를 사용해 컴파일을 함

- 이후 JVM을 사용하여 컴파일된 코드를 실행함

- 개발자가 커맨드 창에서 직접 javac와 java 명령어를 사용하여 컴파일과 실행을 수행할 수도 있음

 

Step 2. 구문 분석 (Parsing)

- 컴파일러는 소스 코드 파일을 읽어 들이고, 코드의 구문을 분석하여 구문 트리를 생성하며 프로그램의 구조를 이해함

- 구문의 문법이 틀렸다면 이때 Syntax Error가 발생하게 됨

 

Step 3. 의미 분석 (Semantic Analysis)

- 이전 단계에서 만든 구문 트리를 사용하여 코드의 의미를 분석하고, 변수 및 메서드의 정의와 참조를 확인함

- 타입이 틀렸다면 이 과정에서 타입 에러가 발생함

 

Step 4. 중간 코드 생성 및 코드 최적화

- 구문 트리를 바이트 코드로 변환하기 전, 중간 단계의 코드를 생성함

- 코드 최적화는 중간 코드를 수정하여 실행 속도를 높이거나 메모리 사용을 줄이는 등, 프로그램이 더 효율적으로 동작하도록 만듦

- 모든 컴파일러가 코드 최적화를 수행하는 것은 아님

 

Step 5. 바이트 코드 생성

- 최종적으로 컴파일러는 중간 코드를 실제로 JVM에서 실행될 수 있는 바이트 코드로 변환함

- .class 파일이 생성됨

 

2.3. 실행하기

- 생성된 바이트 코드는 앞서 설명한 JVM을 통해 실행됨

- 구체적인 실행 과정을 알아보자

 

Step 1. 클래스 로딩 (Class Loading)

- JVM은 먼저 실행한 클래스를 로드함

- 클래스 로더(Class Loader)가 필요한 .class 파일을 파일 시스템에서 읽어와 메모리에 로드함

 

Step 2. 바이트 코드 검증 (Bytecode Verification)

- JVM은 로드한 클래스의 바이트 코드를 검증함

- 바이트 코드가 자바 언어 규약을 따르는지 확인함 (보안과 안정성을 위해)

- 만약 바이트 코드가 규약에 어긋나거나 위험한 작업을 시도하면, 검증 오류가 발생하고 클래스 로딩이 실패함

 

Step 3. 준비 (Initialization)

- 클래스가 검증되면, JVM은 클래스 변수(정적 변수)를 초기화하고, 필요한 메모리 공간을 할당함

- 클래스 변수 초기화는 해당 클래스가 처음으로 사용될 때 한 번만 수행함

 

Step 4. 실행 (Execution)

- 이제 main 메서드를 찾아 실행을 시작함

 

인터프리터 방식

- 초기 실행 단계에서는 인터프리터 방식을 사용하며 바이트 코드를 한 줄씩 읽어들이고 해석함

- 즉, JVM은 바이트 코드 명령을 읽고 해석하여 해당 명령을 실제로 실행함

- 이 과정에서 해석 오버헤드가 발생하며 성능이 비교적 느림

 

JIT 컴파일러 방식

- JVM은 필요한 경우, JIT 컴파일러를 사용하여 성능을 향상시키는 방식을 함께 사용함

- 바이트 코드를 실제 기계어로 번역하는 과정에서 반복되는 코드를 컴파일해두었다가, 해당 코드 블록에 다시 접근할 때 또다시 인터프리터 방식으로 한 줄씩 읽는 것이 아닌, 이미 컴파일된 코드를 사용하는 것 (아래에서 자세히 알아보자) 

 

Step 5. 실행 스택 (Execution Stack)

- JVM은 실행 중인 스레드마다 실행 스택을 관리함

- 각 스레드는 메서드 호출 및 반환을 관리하기 위한 실행 스택을 가짐

- 메서드가 호출될 때마다 호출 스택 프레임이 생성되고, 메서드가 반환될 때 스택 프레임이 제거됨

 

Step 6. 메모리 관리 및 가비지 컬렉션

- JVM은 자동 메모리 관리를 제공함

- 객체가 더 이상 참조되지 않을 때, 가비지 컬렉션을 수행하여 더 이상 필요하지 않은 객체를 정리하고 메모리를 회수함

 

Step 7. 프로그램 종료

- 프로그램이 모든 작업을 완료하면 JVM은 종료되고, 실행 중인 모든 스레드와 프로세스가 종료됨

 

3. JIT 컴파일러란?

이전 "Step 4. 실행" 단계에서의 JIT 컴파일러에 대해 자세히 알아보자

3.1. JIT 컴파일러의 개념

- Just-In-Time Compilation

- JVM 내에는 JIT 컴파일러가 있음

- 바이트 코드를 기계어로 번역하여 실행하는 컴파일러

- JIT 컴파일러는 실행 시간에 동작하며, 코드의 실행 속도를 향상시키는 데 중요한 역할을 함

 

컴파일러 방식

- 소스 코드를 전체를 한 번에 번역하고 목적 코드를 생성하는 방식 (실행x)

인터프리터 방식

- 소스 코드를 실시간으로 한 줄씩 읽어들이고 해석하여 실행하는 방식

 

3.2. JIT 컴파일러의 동작 방식

인터프리터 실행

- JVM 내에서 바이트 코드를 기계어로 처음 번역할 때, JIT 컴파일러는 인터프리터 방식으로 번역함

 

프로파일링

- JIT 컴파일러는 프로그램을 실행하는 동안 프로파일링 정보(실행 빈도, 메서드 호출 빈도, 변수 사용, 메모리 사용량, 실행 시간)를 수집함

 

컴파일

- 프로파일링 정보를 기반으로 가장 빈번하게 실행되는 코드 블록을 선택하여 기계어로 번역함

 

캐싱

- 컴파일된 코드는 캐시에 저장됨

- 이렇게 캐싱된 코드는 나중에 동일한 코드 블록을 실행할 때 재사용함

 

실행

- 컴파일된 코드는 이후에 동일한 코드 블록을 실행할 때 사용함

- 이러한 코드는 인터프리터보다 훨씬 빠르게 실행됨

 

3.3. 정리

- 현대의 JVM은 주로 JIT 컴파일러를 사용함

- 초기 실행 단계에서는 인터프리터 방식을 사용하고, 이후 프로그램이 더 많이 실행되면서 JIT 컴파일러가 컴파일된 코드를 캐싱해 사용하도록 하므로써 성능을 최적화함

- 이러한 방식은 플랫폼 독립성을 유지하면서 높은 실행 성능을 제공하는 데 도움이 됨

 

 

참고자료

https://catch-me-java.tistory.com/9#recentComments

https://catch-me-java.tistory.com/11?category=438116 

https://aboullaite.me/understanding-jit-compiler-just-in-time-compiler/