[Java] JDK, JVM, JRE
Updated: Categories: CS자바의 동작원리에 대해 알아보자
Java 구성요소
JDK
- Java Development Kit, 자바 개발도구
- 컴파일러(javac), 디버그 도구를 포함
JRE
- Java Runtime Environment, 자바 실행환경 (JVM 실행환경을 구현)
- 필수 자바 라이브러리 포함
JVM
- Java Virtual Machine, 자바 가상머신
- OS별로 존재함 (OS에 종속적임)
- 컴파일된 바이트 코드를 읽어 실행함
Java 버전
- 8, 11, 17 버전은 LTS(Long Term Support) 버전이다.
Java 8 특징
- Lambda
- Functional interface
- Default method
- Stream
- Optional
- 새롭게 추가된 날짜 API
- 이전에는 날짜 계산이 쉽지 않았음
- LocalDate, LocalTime, LocalDateTime
- CompletableFuture
- JVM의 변화
- Heap영역에 저장되던 PermGen 영역을 native 영역으로 이동
Java 11 특징
- String 관련 메소드 추가
- File 관련 메소드 추가
- Predicate, static not 메소드 추가
- java 실행 (javac로 컴파일x)
- garbage collector 추가
~17 특징
- switch문 확장(12)
- instanceof 강화(16)
- Record(16)
Java 동작원리
Class Loader
- 클래스 파일을 로드하고 링크를 통해 배치하는 모듈
- 클래스들은 동적로딩됨 (실행시 모든 클래스가 로드되는게 아닌 필요한 시점에 로딩)
Runtime Data Area
- JVM의 메모리 영역으로 5가지 영역으로 분리된다.
- Method와 Heap 영역만이 공유 메모리 영역이다.
- Method
- 클래스, 변수, 메소드, static 변수, constant pool 정보가 저장되는 영역
- 클래스가 로드될때 할당됨
- Heap
- 동적할당된 객체와 배열이 저장되는 영역
- 런타임시 할당됨
- Stack
- 지역변수, 매개변수, 리턴값 등의 임시값이 생성될때 저장되는 영역
- 메소드 호출시마다 개별 스택 프레임이 생성되고, 종료시 해제됨
- 컴파일시 할당됨
- PC register
- 현재 스레드가 실행되는 부분의 주소와 명령을 저장
- 스레드가 생성될때 할당됨
- Native Method Stack
- 자바 외 언어로 작성된 네이티브 코드를 위한 메모리 영역(JNI)
Execution Engine
- JVM 메모리 영역의 바이트 코드를 명령어 단위로 실행함
- 실행방법에는 두가지가 있음
- 인터프리터 : 바이트코드를 하나씩 읽어서 실행 (느림, 초기에 사용)
- JIT(Just-In Time Compiler) : 바이트코드 전체를 바이너리 코드로 컴파일 후 실행 (빠름)
- 자바는 어떻게 동작하나?
- 처음엔 인터프리터 방식으로 바이트코드 -> 바이너리로 변환해 실행
- 해당 코드는 JIT에서 캐싱하고, 중복 코드 발생시 인터프리터가 동작하지 않고 JIT가 바로 캐싱코드를 사용
- JIT는 인터프리터 방식을 보조한다고 생각하면 됨
Java 컴파일과정
- 다른 컴파일 언어는 컴파일러가 OS에 종속적이지만, Java는 모든 OS에서 동일한 Java 컴파일러를 가진다
- 동일하게 컴파일된 바이트 코드를 각 OS별 JVM에서 번역을 해주는 방식
- 컴파일 순서
- java 코드 작성
- 컴파일러(
javac
)가 java 파일을 컴파일- 바이트 코드(
.class
) 생성
- 바이트 코드(
- 바이트코드를 Class Loader에 올림
- 클래스를 Runtime Data Area에 올림
- Execution Engine이 JVM 메모리에 올라온 바이트 코드를 실행
컴파일 언어 vs 인터프리터 언어
컴파일 언어
- 실행 전 빌드과정을 거친다.
- 고급언어를 컴파일하여 모두 기계어(바이너리)로 변환한다
- 장점
- 실행 속도가 빠르다
- 디버깅이 쉽다
- 단점
- 실행 전 빌드시간이 필수적으로 소요된다
인터프리터 언어
- 스크립트 언어라고도 함
- 실행 전 빌드과정없이 고급언어를 인터프리터가 한줄씩 실시간으로 해석하여 실행한다.
- 장점
- 빌드시간이 필요하지 않다.
- 수정 후 바로 재시작하면 되기에 개발속도가 빠르다
- 단점
- 실행 속도가 느리다
- 코드의 오류 인지 시점이 늦고 디버깅이 힘들다
자바는?
- 컴파일과 인터프리터 방식을 둘다 사용
- 소스코드와 기계어 사이의
.class
(바이트 코드)가 존재 - 컴파일 : 소스코드 -> 바이트 코드
- 인터프리터 : 바이트 코드 -> 바이너리 코드
- 소스코드와 기계어 사이의
Garbage Collector
- 힙 영역에서 참조되지 않는 객체를 주기적으로 검사, 제거하는 메모리 관리 기법
- 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다.
- 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.
- 장점
- 개발자가 직접 메모리를 관리하지 않아도 됨
- 메모리 누수 방지
- 해제된 메모리에 대한 접근 방지
- 단점
- 오버헤드가 큼
- GC 동작 시점을 개발자가 알 수 없음
GC 선별 알고리즘 종류
- GC Root(root space)로 부터 참조되지 않는 객체를 선별해내는 알고리즘
- GC Root가 될 수 있는 대상
- Method 영역의 static 변수
- Stack 영역의 로컬 변수
- Native Method Stack 영역의 JNI 레퍼런스
Reference Counting
- root space에서부터 heap 객체의 참조횟수(reference couting)를 기록한다
- 참조횟수가 0인 객체는 GC 대상이 된다
- 단 객체끼리 참조하는 순환참조가 일어날 수 있고, 이렇게 되면 해당 객체는 영영 삭제되지 않는다
Mark and Sweep
- Mark
- 그래프 순회를 통해 사용되는 메모리와 사용되지 않는 메모리를 식별하는 작업
- Reachability를 계산
- Sweep
- Mark 단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업
- 여기서 메모리 단편화를 막기 위한 Compaction이 진행됨
- 장점 : 순환참조 문제를 해결
- 단점 : 의도적으로 GC를 실행해야하며, 어플리케이션과 GC 실행이 병행됨
Major GC 종류
- Major GC에서는 Stop the World가 발생한다.
- 힙 영역의 모든 객체를 검사하기 때문에 시간이 오래걸림
- Minor GC보다 10배의 시간이 소요되며, 다른 스레드를 중단시킴
- Stop the World 발생 횟수를 줄이는 것이 GC 튜닝
Serial GC
Parallel GC
CMS GC
G1 GC
자바의 GC 동작원리
- 자바는 Mark and Sweep 방식으로 GC가 동작함
JVM Heap 영역
- Young Generation
- Eden, Survivor 1,0 영역
- Minor GC 발생 영역
- Old Generation
- Old 영역
- Java 8 이전엔 Perm 영역이 추가로 존재했음
- Major GC 발생 영역
전체 동작 과정
- 새로 생성된 객체는 Eden 영역에 할당됨
- Eden 영역이 꽉차면 Mark & Sweep 실행
- 살아남은 객체들을 Survivor0 영역으로 이동
- 이동시키면서 객체의 age bit를 1 증가시킴
- 다시 새 객체는 Eden 영역에 할당
- Eden 영역이 꽉차면 Eden + Survivor0 영역에서 Mark & Sweep 실행
- 두 영역에서 살아남은 객체들을 Survivor1 영역으로 이동
- 위 과정을 반복하며 살아남는 객체들은 Survivor0, Survivor1 영역에 번갈아가며 이동
- 반복 중 Age bit가 threshold값 이상이 되거나 Survivor 영역의 메모리가 부족해지면 객체를 Old 영역으로 이동
- 이걸 Promotion이라고 함
- Java 8 - Parallel GC 방식 기준 threshold = 15
- Old 영역이 꽉차게 되면 Mark & Sweep (Major GC) 실행