JVM - Execution Engine

김동헌·2024년 4월 15일
2

Java

목록 보기
4/4
post-thumbnail

Class Loader : 컴파일된 바이트 코드를 Runtime Data Area에 로드
Execution Engine은 이 바이트 코드를 실행


Execution Engine

Interpreter & JIT Compiler

Execution Engine은 이 바이트 코드를 기계어로 변환하여 실행

Execution Engine은 바이트 코드를 기계어로 변하는 과정에서 2가지 방법을 사용합니다.

  1. Interpreter : 바이트 코드를 한 줄씩 읽어서 실행
    • 1.1 : 명령어를 처음에 시작하는 속도는 컴파일러보다 빠르지만
      전체적인 수행 속도는 컴파일러보다 느리다는 단점.
    • 1.2 : 같은 메서드가 수행된다 하더라도 새로 해석하여 수행한다.
  2. JIT(Just-In-Time) Compiler : 바이트 코드의 실행 빈도를 분석하여, 자주 사용되는 코드를 더 효율적으로 기계어로 변환
    • 2.1 : 1.2의 단점을 보완하기 위해, Interpreter로 바이트 코드를 수행하다가 적절한 시점(임계치)이 오면, 바이트 코드 전체를 컴파일하여 네이티브 코드로 변환한다.
      이후 같은 바이트 코드를 수행하게 되면 인터프리터 과정을 하지 않고 컴파일된 네이티브 코드를 수행한다.

또한 Execution EngineRuntime Data Area의 Heap 메모리 관리를 위해 Garbage Collector를 포함하고 있어, 사용하지 않는 메모리 자원을 자동으로 회수하여 메모리를 효율적으로 관리합니다.

JIT Compiler의 동작

    void jitCompilerTest() {
        int a = 0;
        for (int i = 0; i < 500; i++) {
            long startTime = System.nanoTime();
            for (int j = 0; j < 10000; j++)
                a++;
            long endTime = System.nanoTime();
            System.out.printf("%d\t%d\n", i, endTime - startTime);

        }
    }

해당 코드는 메서드가 수행될 때 시간을 측정하는데, 상위 for문이 실행될 때마다 내부에서 a에 1을 더하는 연산을 10,000번 수행하고, 소요되는 시간을 출력합니다.

실행 결과는 다음과 같습니다.

// 0~10의 결과
0	103900
1	126000
2	124800
3	124600
4	110100
5	108500
6	118600
7	111900
8	96200
9	18900
10	11700
// 76~499의 결과
76	24000
77	11400
78	11400
79	56800
80	600
81	0
82	100
83	100
84	0
85	100
86	100
...
495	0
496	100
497	0
498	0
499	100

JIT Compiler인터프리터의 같은 메서드가 수행되더라도 새로 해석하여 수행한다는 단점을 보완하기 위해서 나왔다고 설명했었습니다.

결과를 통해 확인해보니, 시간이 점차 줄어들더니 80번째부터는 연산 시간이 매우 단축되었습니다.

시간 단축의 과정은 아래와 같습니다.

  1. Interpreter : 초기 실행은 인터프리터가 바이트 코드를 한 줄씩 읽어서 실행합니다. 이 때 각 바이트 코드의 실행 횟수 등을 분석합니다.
  2. 컴파일 임계치 도달 : 바이트 코드의 실행 빈도 등을 기반으로 컴파일 임계치를 설정합니다. 임계치는 메서드 호출 횟수 또는 반복문 수행 빈도 등으로 결정될 수 있습니다.
  3. 네이티브 코드 변환 : 임계치에 도달한 바이트 코드는 JIT Compiler에 의해 네이티브 코드(기계어)로 변환됩니다.
    변환된 코드는 메모리에 저장되어, 같은 바이트 코드가 다시 실행될 때 더 빠르게 처리됩니다.

이렇게 변환된 네이티브 코드는 인터프리터를 거치지 않고 직접 실행하기 때문에 속도가 크게 향상되는 것을 결과로 확인할 수 있습니다.

JIT Compiler는 Java 벤더에 따라 구현되어 있는 방법은 다르지만 JIT Compiler의 명세에 따라 각 Java의 벤더가 구현되어야 합니다.


Garbage Collector

가비지 컬렉션은 프로그래머가 직접적으로 메모리를 할당하고 해제하는 것 대신, 프로그램 실행 중에 더 이상 필요하지 않은 메모리를 자동으로 식별하고 해제합니다.

Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객체(garbage)를 모아 주기적으로 제거

이를 통해

  • 프로그래머는 메모리 관리에 대한 부담을 줄이고,
  • 런타임 시스템이 자동으로 메모리를 관리

를 하도록 할 수 있습니다.

따라서 가비지 컬렉션은 프로그램이 실행되는 동안 발생하는 쓸모 없어진 메모리를 효율적으로 관리하여 프로그램의 성능과 안정성을 향상시키는 데 도움을 줍니다.

Java에서는 가비지 컬렉터를 이용해 Java 프로세스는 한정된 메모리를 효율적으로 사용할 수 있고, 개발자 입장에서는 메모리 관리, 메모리 누수(Memory Leak) 문제에서 대해 관리하지 않아도 되서 개발에만 집중할 수 있습니다.

좀 더 자세한 내용은 Garbage Collection에 포스팅 하였으며, JVM은 아래 전제 조건의 2가지를 따르면서 Mark and Sweep 방식을 따릅니다.

약한 세대 가설(Weak Generational Hyphothesis)

  • 대부분의 객체는 금방 Unreachable상태가 된다.
  • 오래된 객체에서 젋은 객체로의 참조는 아주 적게 존재한다.

참고 자료
https://www.javatpoint.com/java-interpreter
https://docs.oracle.com/javase/specs/jvms/se17/html/
https://medium.com/@gsy4568/jvm%EC%9D%98-%EB%8F%99%EC%9E%91%EC%9D%80-execution-engine-ed2480176a15

profile
백엔드 기록 공간😁

0개의 댓글