- JVM은 자바 언어로 작성된 프로그램을 실행하는 역할을 한다.
- 자바 프로그램은 자바 컴파일러에 의해
바이트코드
로 변환되고, JVM은 이바이트코드
를 실행한다. - JVM은 플랫폼 독립성을 제공하여, 한 번의 컴파일로 여러 운영체제에서 실행 가능하다.
- JVM은 크게 클래스 로더, 실행 엔진, 메모리 영역으로 구성된다.
- 클래스 로더는 클래스 파일을 JVM으로 로드하고, 메모리에 적재한다.
- 실행 엔진은
바이트코드
를 실행하는 역할을 담당한다.- 주요한 실행 엔진 - 인터프리터, JIT 컴파일러
- 메모리 영역은 프로그램 실행에 필요한 메모리를 관리한다.
- 주요 영역 - 메소드, 힙, 스택
- 자바 소스 코드는 자바 컴파일러에 의해 컴파일되어 바이트코드로 변환된다.
- 바이트코드는 JVM이 이해할 수 있는 중간 언어로, 기계어에 가깝지만 플랫폼 독립적이다.
- JVM은 바이트코드를 실행하여 프로그램을 실행한다.
- 클래스 로딩은 JVM이 클래스 파일을 로드하여 메모리에 적재하는 과정이다.
- 클래스 로더는 로딩, 링크, 초기화의 3단계로 이루어진 로딩 과정을 수행한다.
-
로딩
JVM 클래스 파일을 찾아 메모리로 읽어오는 과정이다.
클래스 로더가 클래스 파일을 검색하고, 해당 클래스 파일을 JVM 내부의 메모리에 로드한다.
로드된 클래스는 JVM 내부에서 사용될 수 있다.
-
링크
로딩된 클래스의 준비 작업을 수행하는 단계이다.
-
검증
로딩된 클래스 파일이 자바 언어 명세에 맞는지 확인한다.
-
준비
클래스의 정적 변수를 메모리에 할당하고 기본 값으로 초기화한다.
-
해결
클래스 레벨에서의 참조를 실제 메모리 상의 참조로 연결한다. 다른 클래스나 메서드에 대한 참조를 실제 메모리 주소와 연결한다.
-
-
초기화
클래스 변수와 클래스의 정적 초기화 블록을 실행하는 단계이다.
초기화는 클래스의 인스턴스가 생성되기 전에 실행된다.
정적 초기화 블록은 클래스가 처음으로 로딩될 때 한 번만 실행된다.
-
- 클래스 로더는 클래스 파일을 검색하고 로드하는 역할을 한다.
- JVM은 실행 중인 자바 프로그램에 대한 메모리를 관리한다.
- 주요 메모리 영역으로 Heap, Stack, Method Area, Native Method Stack 등이 있다.
-
Heap
동적으로 할당되는 객체 인스턴스와 배열이 저장되는 영역
-
Stack
메서드 호출과 관련된 정보를 저장하는 영역
각 스레드마다 별도의 스택이 할당된다.
-
Method Area
클래스의 구조와 정적 변수를 저장하는 영역
-
Native Method Stack
자바 코드가 아닌 다른 언어로 작성된 네이티브 메소드 호출 시 사용되는 스택
-
- 자바 프로그램에서 메모리가 부족한 상황에서 발생하는 예외
- 힙 메모리 사용량이 최대치거나, 메모리 누수가 발생한 경우
-
메모리 누수
더 이상 필요하지 않은 객체가 메모리에서 회수되지 않고 유지되는 상황을 의미한다.
이를 방지하기 위해, 가비지 컬렉션 및 메모리 최적화 기법을 적절하게 사용하고, 객체 참조를 관리해야한다.
-
수동으로 메모리를 관리하는건 번거롭고 어려운 일이다..
JAVA에서는 GC가 Heap 메모리에서 unreachable한 객체를 삭제시켜준다.
코드 레벨의 메모리 관리에서 벗어나 편리하다.
- Memory Leak가 발생되지 않음
- 휴먼 에러 발생 가능성 낮춤
-
성능 저하
어떤 메모리를 해제할지 검사하고 삭제하는 과정 또한 결국 CPU 자원과 메모리를 필요로 한다.
-
개발자는 언제 메모리가 해제되는지 모른다.
jvm은 GC를 실행시키기 위해 잠시 application 실행을 멈춘다.
Mark : root set으로부터 Heap 영역의 모든 객체를 스캔한다.
Sweep : root set에서 unreachable한 객체를 Heap 영역에서 제거한다.
- 의도적으로 GC를 실행시켜야한다.
Young generation : 새로운 객체들이 할당되는 곳
Old generation : Young generation에서 오랫동안 살아남은 객체들이 존재하는 곳
- application 실행과 GC 실행이 병행된다.
- 할당된 객체는 오랫동안 참조되지 않는게 대부분이다. 즉, 금방 Garbage 상태가 된다.
- 반대로 오래된 객체에서 젊은 객체로의 참조는 거의 없다.
- 따라서 Heap이 하나라면 오래된 객체까지 스캔해야해서 비효율적이다.
- 차라리 두 영역으로 나눠서 오래된 객체는 따로 빼두고, 할당된지 얼마 안 된 객체들만 주기적으로 스캔하는 게 훨씬 효율적이다.
-
eden에 객체가 꽉 차면 minor GC가 실행된다.
- mark and sweep이 진행된다.
- reachable이라 판단된 객체는 survival 0 영역으로 옮겨진다.
- 이 때 옮겨지면서 age-bit가 1 올라간다.
- age-bit가 특정 숫자만큼 높아지면, old generation으로 옮겨진다.
-
eden이 또 꽉차면?
- reachable이라고 판단된 객체들이 survival 1 영역으로 이동하며 age-bit가 1 올라간다.
- survival 0 영역에 있던 친구들도 survival 1 영역으로 이동하며 age-bit가 1 올라간다.
generation이 꽉 차면 major GC가 실행되며 mark and sweep을 이용한다.
JVM은 GC를 실행하기 위해 Application 실행을 멈춘다 (stop the world)
stop the world 시간이 짧을 수록 최적화된 것이다.
-
parallel GC
여러 개의 thread로 GC를 실행하기 때문에 stop the world 시간이 짧다.
-
G1 GC
힙 영역을 더 작은 영역인 리전이라는 단위로 분할하여 관리한다.
Just In Time 컴파일러는 JVM 실행 엔진 중 하나로, 바이트코드를 실제 기계어로 변환하여 실행 속도를 향상시키는 역할을 한다.
JIT 컴파일러 동작 원리
- 인터프리터 실행
- 프로그램이 시작되면 인터프리터에 의해 바이트코드가 해석되고 실행된다.
- 프로파일링
- 인터프리터는 실행된 코드의 횟수, 타입 등의 정보를 수집하여 프로그램의 실행 특성을 분석한다.
- 컴파일 타깃 선택
- 프로파일링 정보를 기반으로, 최적의 성능을 위해 컴파일할 코드를 선택한다.
- 동적 컴파일
- 선택된 코드는 JIT 컴파일러에 의해 동적으로 기계어로 변환된다.
- 기계어 실행
- 컴파일된 코드는 바로 실행되며, 인터프리터의 해석 단계를 거치지 않고 직접 실행된다.
- 실행 시간을 개선하여 애플리케이션 성능 향상
- 코드를 동적으로 최적화하므로, 최적화 수준이 높아진다.
- JIT 컴파일러의 컴파일 과정이 약간의 오버헤드를 발생시키므로, 초기 실행 시간이 늘어날 수 있다.
- JIT 컴파일러가 동적으로 생성하는 기계어 코드는 캐시 메모리를 많이 사용하므로, 메모리 사용량이 증가할 수 있다.
GC는 가장 많은 가비지를 가지고 있는 리전을 우선적으로 처리한다. (Garbage-First)
G1 GC는 일시 정지 시간을 최소화하기 위해 병렬 및 동시 가비지 컬렉션을 사용한다.
일시 정지 시간을 분산시켜 애플리케이션의 반응성을 향상시킨다.