자바

[JVM] 실행 엔진 알아보기 - (3) / 인터프리터, JIT Compiler, GC

누누01 2023. 5. 17. 22:59
728x90

실행 엔진은 Runtime Data Area 에 적재된 바이트코드들을 실행하고 사용하지 않는 데이터들을 제거하는 역할을 하며 세 가지 장치로 구분된다.

 

- 인터프리터(Interpreter)
- JIT Compiler
- Garbage Collector

 


인터프리터(Interpreter)

JVM 인터프리터는 런타임 중 바이트코드를 한 라인씩 읽고 Native Code 로 변환하는 작업을 수행한다.

인터프리터는 한 줄씩 기계어로 번역하는 만큼 번역속도는 빠르지만 전체 실행속도는 느리다. 또한 중복되는 바이트코드들에 대해서도 매번 컴파일을 하기 때문에 비효율적이라는 단점이 있다.

 

이러한 인터프리터 방식의 단점을 보완하기 위해 자바는 1.2 버전부터 뒤이어 설명할 JIT Compiler 를 출시하고 지속적으로 업데이트하면서 속도의 많은 부분이 개선되었다.

 

 

 


 

JIT Compiler(Just In Time)

인터프리터 방식의 속도 문제를 해결하기 위해 디자인된 기능이다.

JIT Compiler 는 인터프리터 동작 중 자주 실행되는 바이트 코드 영역을 native code 로 번역해 캐싱해두는 역할을 한다.

 

JIT Compiler 는 이 자주 실행되는 기준을 '컴파일 임계치' 라는 개념의 단위로 판단한다.

 

컴파일 임계치를 구하는 방법은 다음과 같다.

- method entry counter: JVM 내에 있는 메서드가 호출된 횟수
- back-edge loop counter: 메서드가 루프를 빠져나오기까지 회전한 횟수

 

JIT Compiler 는 컴파일 임계치가 일정 횟수에 도달한 코드를 캐싱하기 충분하다고 판단한다.

캐싱하기 충분하다고 판단된 코드는 컴파일 스레드에 의해 컴파일되기를 기다렸다가 실행되게 된다.

 

이 컴파일 임계치는 JVM 옵션을 통해 관련된 값을 조절할 수 있다.

 

 

더 자세한 옵션값은 여기

(https://mg-laboratory.tistory.com/199)

 

 


 

Garbage Collector

프로그램이 실행되다 보면 유효하지 않은 메모리인 가비지가 발생하게 되는데 이러한 불필요한 객체들(unreachable objects)을 분별해내고 정리해주는 것이 JVM 의 Garbage Colloector 이다.

 

후에 설명하겠지만 GC 가 시작되면 JVM 은 GC 를 실행하는 쓰레드를 제외한 모든 쓰레드들의 작업이 중단된다.

이러한 특징으로 인해 GC 실행 시간을 줄이고자 다양한 알고리즘이 등장하였고 이 글에서는 몇 가지 중요한 알고리즘과 특징들에 대해 간략하게 설명하겠다.

 

 

 

 

GC 의 동작 방식을 이해하기 위해서는 Heap Area 가 어떻게 이루어져 있는지 알 필요가 있다.

Heap 영역은 다음과 같은 형태로 이루어져 있다.

 

 

 

(이전 글에서 보았다시피 JDK 8 이후로 Perm Gen 이 사라졌다는 사실을 명심하자)

 

 

크게 Yong 영역 + Old 영역으로 이루어져 있으며, Yong 영역은 1개의 Eden 과 2개의 Survivor 영역으로 나뉘어져 있다.

 

Step 1 - GC 의 특징

Yong 영역과 Old 영역은 서로 다른 메모리 구조로 되어있기 때문에 세부적인 GC 동작 방식은 다르다. 하지만 기본적으로 GC 가 진행된다고 했을 때 다음의 두 가지 공통적인 특징을 가진다.

 

- Stop The World
- Mark and Sweep

 

1. Stop The World

Stop The Wolrd 는 JVM 이 GC 를 실행하기 위해 애플리케이션 실행을 멈추는 작업이다.

GC 가 실행될 때는 GC 를 실행하는 thread 를 제외한 모든 thread 들의 작업이 중단되고, GC 가 완료되면 작업이 재개된다.

보통 GC 의 성능 개선을 위한 튜닝을 한다고 하면 이 시간을 줄이는 작업을 하는 것이다.

 

2. Mark and Sweep

Stop The World 를 통해 모든 작업을 중단시키면, GC 는 스택의 모든 변수 또는 reachable 객체를 스캔하면서 각각 어떤 객체를 참조하고 있는지 탐색한다.

그리고 사용되고 있는 객체들을 식별하는데, 이러한 과정을 Mark 라고 한다.

이후에 Mark 되지 않은 객체들을 메모리에서 제거하는 과정을 Sweep 이라고 한다.

 

 

 

Step 2 - GC 의 동작 방식

GC 는 Yong 영역과 Old 영역에서 일어나는 GC 작업을 구분한여 진행한다.

Yong 영역에서 일어나는 GC 를 Minor GC 라 하고, Old 영역에서 일어나는 GC 를 Major GC 또는 Full GC 라 한다.

 

1. Minor GC

Minor GC 는 Heap Area 의 Yong Generation 에서 일어난다.

Yong 영역은 새롭게 생성되는 객체가 Allocation(할당) 되는 영역이다.

대부분의 객체가 이 영역에서 금방 접근 불가능한 상태(Unreachable) 가 되기 때문에, 많은 객체가 Yong 영역에서 GC 의 대상이 된다.

Minor GC 가 실행되면 참조되고 있는 객체들을 제외하고 나머지 객체들은 제거하게 된다.

 

 

Minor GC 가 진행되는 세부 과정은 다음과 같다.

 

 

1. 새로 생성된 객체가 Eden 영역에 할당된다.
2. 객체가 계속 생성되다가 Eden 영역이 꽉 차게 되고 Minor GC가 실행된다.
   - Eden 영역에서 사용되지 않는 객체의 메모리가 해제된다.
   - Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동된다.
3. 1~2 번 과정이 반복되다가 Survivor 영역이 가득 차게 되면 Survivor 영역의 살아남은 객체를 Survivor 영역으로 이동시킨다.
    (반드시 1개의 Survivor 영역에만 데이터가 존재해야 한다.)
4. 이러한 과정을 반복해 계속해서 살아남은 객체는 Old 영역으로 Promotion(이동) 하게 된다.

 

이 과정 중 주의해야 할 점은 Survivor 영역 중 1 개는 반드시 사용이 되어야 한다는 것이다. 만약 두 Survivor 영역에 모두 데이터가 존재하거나, 모두 사용량이 0 이라면 그 시스템은 정상이 아니라는 것을 알 수 있다.

 

또한 Object Header 에 Minor GC 에서 객체가 살아남은 횟수를 의미하는 age 가 기록되어 다음 Minor GC 에서 Promotion 여부를 결정하는 변수가 된다.

 

 

 

2. Major GC(Full GC)

Major GC 는 Old Generation 에서 일어나는 GC 를 말한다.

Old 영역은 Yong 영역에서 Reachable 상태를 유지하며 살아남은 객체들이 복사되는 영역으로, Yong 영역보다 크게 할당되며 영역의 크기가 크고 거의 대부분의 객체들이 Yong 영역에서 GC 의 대상이 되는 만큼 가비지는 적게 발생한다.

그러나 객체들이 계속 Promotion 되어 Old 영역 메모리가 부족해지고, Major GC 가 발생하면 그 시간은 Minor GC 보다 훨씬 오래걸린다.

 

Step 3 - GC 의 다양한 알고리즘

1. Serial Collector

싱글 스레드로 모든 종류의 GC 를 수행하는 방식이다.

싱글 프로세서 시스템에 가장 적합하다.

Serial GC 는 모든 GC 일을 처리하기 위해 1개의 thread 만을 이용하기 때문에 CPU 코더가 여러개인 운영 서버 Serial GC 를 사용하는 것은 반드시 피해야 한다.

 

  • Yong Generation Collection Algorithm: Serial
  • Old Generation Collection Algorithm: Serial Mark-Sweep-Compact

 

2. Parallel Collector

멀티 프로세서나 멀티 스레드 하드웨어에서 돌아가는 중대형 규모의 데이터셋을 다루는 애플리케이션을 위한 GC 방식이다.

Parallel Collector의 목표는 다른 CPU가 GC의 진행시간 동안 대기 상태로 남아있는 것을 최소화 하는 것이다.

이를 위해 Minor GC를 병렬로 수행하게 하여 GC의 오버헤드를 현저하게 줄이고 성능을 향상시키게 한다.

이 방식을 좀 더 향상시킨 방식이 바로 Parallel Compaction Collector 이다.

Parallel Compaction Collector는 Parallel Collector 에서 Major GC를 병렬로 수행하게 해주는 방식이다.

Parallel GC는 GC의 오버헤드를 상당히 줄여주어 Java8 까지 기본 GC로 사용되었다.

그러나 Application이 멈추는 현상은 지속되어 더 나은 알고리즘이 개발되게 된다.

 

  • Yong Generation Collection Algorithm: Parallel Scavenge
  • Old Generation Collection Algorithm: Serial Mark-Sweep-Compact

 

3. Concurrent Mark-Sweep (CMS)

CMS Collector 는 힙 메모리의 크기가 클 때 적합하다.

GC 일시 정지가 짧은 것을 선호하는 애플리케이션을 위한 컬렉터이다.

GC의 일시정지 시간을 줄이는 것이 목적이며, 이 방식은 프로세서 리소스를 GC와 공유하게 된다.

이러한 방식은 자원이 GC를 위해 사용되므로 응답이 느려질 순 있지만 멈추지는 않게 된다.

그러나 CMS는 다른 GC 방식보다 메모리와 CPU를 많이 필요로해 여러 문제들이 있었고 결국 Java 9 버전부터 deprecated 되었다.

Java 14에서는 사용이 중지되었다.

 

4. Garbage-First Garbage Collector (G1GC)

G1 GC는 장기적으로 많은 문제를 일으킬 수 있는 CMS GC 를 대체하기 위해 개발되었고, Java 7 부터 지원되기 시작했다.

이 컬렉터는 Yong 영역과 Old 영역으로 나누는 방식을 사용하지 않고 전체 Heap을 1MB 단위의 region(리전)들로 균등하게 나눈다.

그리고 각 지역을 역할과 함께 논리적으로 구분하여 객체를 할당한다.

G1 GC 에서는 Eden, Survivor, Old 역할에 더해 Humongous 와 Available/Unused 라는 두 가지 역할이 추가되는데,

Humongous 는 region 크기가 50%를 초과하는 객체를 저장하는 region을 의미하며 Available/Unused는 사용되지 않은 region을 의미한다.

G1 GC 의 핵심은 Heap을 동일한 크기의 region으로 나누고, 새로 추가된 역할을 참고하여 가비지가 많은 region에 의해 우선적으로 GC를 수행하는 것이다.

 

G1 GC는 Minor GC와 Major GC를 수행하지만, 다른 GC와는 차이점이 존재한다. 그에 대해 살펴보자.

 

 

1. Minor GC

 

Eden 지역에서 GC가 수행되면 살아남은 객체를 Mark(식별) 하고, 메모리를 Sweep(회수) 한다. 그리고 이 객체들을 다른 지역으로 이동시키게 된다.

복제되는 지역이 Available/Unused 지역이면 해당 지역은 Survivor 영역이 되고, Eden 영역이면 Available/Unused 지역이 된다.

한 지역에 객체를 할당하다가 해당 지역이 꽉 차면 다른 지역에 객체를 할당하고, Minor GC를 실행한다. G1 GC는 각 지역을 추적하고 있기 때문에, Garbage First(가비지가 가장 많은 지역) 를 찾아서 Mark and Sweep 을 수행한다.

 

2. Major GC (Full GC)

 

여기에서 G1 GC의 장점이 드러난다.

기존 다른 GC 들은 모든 Heap 영역에서 GC가 수행되어 처리 시간이 상당히 오래 걸렸지만, G1 GC는 어느 영역에 가비지가 많은지 추적하고 있기 때문에 GC를 수행할 지역을 조합하여 해당 지역에 대해서만 GC를 수행한다.

시스템이 계속 운영되다가 객체가 너무 많아 빠르게 메모리를 회수할 수 없을 때 Major GC(Full GC) 가 실행된다.

 

 

 

이러한 방식의 G1 GC는 앞의 어떠한 GC 방식보다 처리 속도가 빠르며, 큰 메모리 공간에서 멀티 프로세스를 기반으로 운영되는 애플리케이션에서도 안정적으로 운용이 가능하기 때문에 Java9부터 기본 GC로 사용되게 되었다.