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/
'Computer Science' 카테고리의 다른 글
[Database] 5. 트랜잭션의 격리 수준과 락 (0) | 2023.09.30 |
---|---|
[Database] 4. 트랜잭션과 무결성 (0) | 2023.09.12 |
[Database] 3. 정규화 (0) | 2023.09.07 |
[Database] 2. 관계형 데이터베이스 설계하기 (0) | 2023.09.06 |
[Database] 1. 데이터베이스가 필요한 이유 (0) | 2023.09.05 |