[JAVA] JVM - Execution Engine

impala·2023년 1월 2일
0

JAVA

목록 보기
5/6
post-thumbnail

Execution Engine

Class Loader에 의해 Method Area에 로드된 class file은 클래스의 정보를 담은 Constant Pool과 클래스의 동작을 정의한 Byte Code를 포함하고 있다. 이때, Execution Engine은 Method Area에 로드되어있는 Byte Code를 명령어 단위로 읽어와 실행한다.

JVM의 Execution Engine은 세 가지 부분으로 구성되어있다.

  • Interpreter : Byte Code의 명령어를 한줄씩 읽어 기계어로 번역함
  • JIT Compiler : Interpreter를 보완하기 위해 중복되는 Byte Code들을 한꺼번에 번역함
  • Garbage Collector : 효율적인 메모리관리를 위해 사용하지 않는 메모리를 자동으로 정리함

Interpreter

Byte Code를 한줄씩 읽어서 Native Code(기계어)로 번역하는 역할을 담당한다. 하지만 반복적으로 호출되는 메소드를 매번 번역해야하기 때문에 효율성이 떨어져 JIT Compiler를 도입했다.


JIT Compiler

JIT : Just In Time

JIT Compiler는 Interpreter와 같이 Byte Code를 Native Code로 번역하는 일을 담당한다. 하지만 Interpreter와 달리 매번 코드를 번역하지 않고, 자주 반복되는 코드는 Native Code로 번역하여 캐시(Native Method Stack)에 저장해두었다가 같은 코드가 나왔을 때 캐싱을 통해 이미 번역된 Native Code를 사용하여 효율성을 높였다.

JIT Compile

JVM Execution Engine은 초기에 Interpreter방식으로 동작하다가 특정 조건(컴파일 임계치)을 만족하면 적절한 시점에 Byte Code전체를 Native Code로 바꾸어 캐시에 저장한다. 그 다음부터 Interpreter는 Native Code를 그대로 가져다 사용한다. Native Code를 실행하는 것이 인터프리팅 방식보다 빠르고, Native Code는 캐시에 보관하기 때문에 한번 컴파일된 코드는 계속 빠르게 수행된다


컴파일 임계치

JIT Compile을 수행할 기준. method entry counter와 back-edge loop counter의 합

  • method entry counter : JVM내에 있는 메소드가 호출된 횟수
  • back-edge loop counter : 메소드가 루프를 빠져나오기 전까지 회전한 횟수
  • 컴파일 임계치가 일정 횟수에 도달한 코드는 컴파일 될 자격이 있다고 판단하여 큐에 들어가 대기하고, 이후 컴파일 쓰레드에 의해 컴파일된다

On-Stack Replacement(OSR)

스택 프레임을 컴파일된 것으로 교체하여 속도를 개선하는 작업

OSR

대상 코드가 컴파일이 완료가 되었지만 최적화되지 않은 코드가 수행되고 있는 것이 발견된 경우 OSR을 수행한다


JIT Compiler의 동작

JIT

JIT Compiler는 위와 같은 구조로 동작한다.

  • Intermediate Representation Generator : JIT Compiler 내부적으로 IR이라는 중간 코드를 통해 소스코드를 표현한다
  • Optimizer : IRG에 의해 생성된 코드를 최적화한다
  • Code Generator : 최적화된 중간코드를 Native Code로 번역한다
  • Profiler : Hot Spot(반복 호출되는 메소드)을 찾는 역할을 담당한다

Compile Option

JVM은 캐시공간을 효율적으로 사용하기 위해 모든 코드를 컴파일하지 않고, Profiling을 통해 자주 사용되는 코드를 찾아 그 빈도와 복잡도에 따라 코드의 레벨을 1~4로 나눈 뒤, 각 Level에 맞는 컴파일러를 사용하여 코드를 번역한다.

JVM의 JIT Compiler의 종류는 C1, C2 두 가지가 있다.


C1 Compiler(Client Compiler) - Compile

스타트업 시간과 메모리 공간 최적화

프로그램의 시작시간을 최소화하는데 집중하기 위해 Byte Code로부터 최대한 많은 정보를 뽑아 실제 동작하는 코드 블럭에 대한 최적화에 집중하며, 전체적인 최적화는 C2 Compiler가 담당한다

C1 컴파일러의 특징은 다음과 같다

  • Level1~3 코드에 대한 컴파일을 수행하지만 캐싱은 하지 않는다
  • 적극성이 높아 서버 컴파일러보다 먼저 컴파일을 시작
  • 최적화를 위한 대기시간이 짧음
  • 코드 분석 및 컴파일 시간이 빠르고, 메모리의 사용량이 적다

C1 컴파일러가 컴파일을 수행하는 순서는 아래와 같다.

  1. Byte Code를 해석한 뒤 최적화를 쉽게 하기 위해 HIR이라는 정적 바이트 코드 표현을 만든다
  2. HIR로부터 플랫폼에 종속적인 중간표현식(LIR)을 만든다
  3. LIR을 사용해 기계어를 생성한다

C2 Compiler(Server Compiler) - Compile + Caching

다수의 request를 빠르게 처리

장기 실행되는 서버측 엔터프라이즈 애플리케이션의 경우 C1 컴파일러로는 최적화가 충분하지 않을 수 있다. 따라서 C2 컴파일러는 부분적인 코드의 실행보다는 전체적인 성능 최적화에 중점을 둔다.
C2 컴파일러는 단기 실행되는 경량 클라이언트 애플리케이션보다 더 많은 프로파일링 데이터를 수집할 수 있기 때문에 더 고급 최적화 기술과 알고리즘을 적용할 수 있다.

C2 컴파일러의 특징은 다음과 같다

  • Level4코드에 대해서만 컴파일과 캐싱을 수행한다
  • 컴파일 전에 많은 정보를 수집하여 전체적인 코드 최적화에 중점을 둔다

정리

C1 컴파일러는 비교적 사용시간이 적은 클라이언트측 어플리케이션을 타겟으로 하여 빠른 컴파일을 통해 어플리케이션이 시작하는 시간을 줄이는 데에 초점을 맞춘 컴파일러이고,

C2 컴파일러는 사용시간이 긴 서버측 어플리케이션을 타겟으로, 컴파일에 걸리는 시간은 비교적 느리지만 높은 수준의 코드 최적화를 통해 전체적인 코드 실행시간을 줄이는 데에 초점을 맞춘 컴파일러이다.

  • Tired Compiler : C1 컴파일러와 C2 컴파일러의 장점을 조합한 컴파일러. C1 컴파일러로 스타트업 시간을 줄이고, 자주 사용되는 부분은 C2 컴파일러로 재컴파일하는 방식.

Garbage Collector

C/C++는 개발자가 직접 메모리를 할당하고 해제해야 하지만(c : malloc/free, c++ : new/delete) Java는 사용자 대신 Garbage Collector가 동작하여 더이상 사용되지 않는 Heap영역의 인스턴스를 찾아 메모리를 해제한다.
JVM의 Garbage Collector에 대한 자세한 내용은 Garbage Collection에 정리했다.


참고자료

4개의 댓글

comment-user-thumbnail
2023년 7월 27일

Byte Code를 한 줄씩 읽어 기계어로 번역합니다. Geometry Dash 하지만 반복적으로 호출되는 메소드를 매번 번역해야 하기 때문에 성능 면에서 효율이 떨어질 수 있습니다.

답글 달기
comment-user-thumbnail
2023년 11월 15일

종합적으로, Execution Engine은 이러한 세 가지 구성 요소를 통해 Java suika game 프로그램을 효율적으로 실행하고 관리합니다. Interpretation은 빠르게 시작할 수 있지만, JIT Compilation은 반복 코드에 대한 최적화를 통해 실행 시간 성능을 향상시킵니다. Garbage Collector는 메모리 관리를 담당하여 프로그래머가 명시적으로 메모리를 해제하는 부담을 줄여줍니다.

답글 달기
comment-user-thumbnail
2024년 3월 17일

I'm glad you find the blog cool and that you feel fortunate to have stumbled upon it, discovering some awesome information along the way xciptv player.

답글 달기
comment-user-thumbnail
2024년 7월 23일

When a Java program is compiled, it is transformed into bytecode, which is platform-independent. The execution engine takes this bytecode and interprets or geometry dash compiles it into machine code that can be executed by the underlying hardware.

답글 달기