Java 코드 실행 과정

itonse·2024년 6월 4일
0

JAVA

목록 보기
19/19

용어 정리

바이트 코드

  • JVM과 같은 가상머신이 이해할 수 있는 언어이다.

네이티브 코드

  • 특정 운영 체제에서 직접 실행될 수 있는 기계어 코드 이다.
  • CPU가 직접 실행할 수 있는 코드로, 성능이 최적화된 형태이다.



자바 컴파일러 (javac) 를 통한 컴파일

.java 파일을 작성하고 자바 컴파일러의 javac명령어를 사용하여 텍스트 파일로 작성된 java 파일을
바이트 코드로 컴파일 합니다. 컴파일된 파일은 class 확장자를 가지며, JVM이 이해할 수 있는 형태로 저장됩니다.

바이트 코드의 각 명령어는 1바이트 크기의 Opcode와 추가 피연산자로 구성됩니다.

아래 이미지는 Main.java 파일을 javac를 이용하여 컴파일한 후, 추출된 Main.class 파일의
바이트 코드를 보여줍니다.

1) 소스코드를 작성하여 저장 -> Main.java

2) javac를 이용한 컴파일

3) Main.class 파일의 바이트 코드



JVM의 클래스 로더 동작

JVM의 클래스 로더는 컴파일된 바이트 코드를 메모리에 로드하고, 검증, 준비, 분석, 초기화 과정을 통해 실행 준비를 완료합니다. 이 과정에서 필요한 클래스들(예: 사용자 정의 클래스, 라이브러리 클래스)을 동적 로딩을 통해 로드하고 링크하여 JVM의 런타임 데이터 영역에 올립니다.

클래스 로더 세부 동작

  1. 로드: 클래스 파일을 가져와서 JVM의 메모리에 로드

  2. 검증: 자바 언어 명세(Java Language Specification) 및 JVM 명세에 맞게
    구성되어 있는지 검사

  3. 준비: 클래스가 필요로 하는 메모리를 할당 (필드, 메서드, 인터페이스 등)

  4. 분석: 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경

  5. 초기화: 클래스 변수들(static 필드)을 적절한 값으로 초기화



실행 엔진(Execution Engine) 동작

실행엔진은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서
실행
합니다. 이때, 두가지 방식으로 명령어를 처리합니다.

1. 인터프리터

바이트 코드 명령어를 하나씩 읽어서 해석하고 실행합니다. 하나하나의 실행은 빠르나, 전체적인
실행 속도가 느리다는 단점을 가집니다.

2. JIT 컴파일러(Just-In-Time Compiler)

인터프리터의 단점을 보완하기 위해 도입된 방식으로, 바이트 코드 전체를 컴파일하여 각 OS에 맞는
네이티브 코드로 변환
한 후 해당 메서드를 네이티브 코드로 직접 실행합니다.
이는 인터프리터 방식보다 전체적인 실행 속도를 빠르게 합니다.

모든 코드를 JIT 컴파일러 방식으로 실행하지 않고, 인터프리터 방식을 사용하다 일정 사용기준을
넘어가면 JIT 컴파일러 방식으로 실행합니다. 또한 실행할 때 컴파일한 코드를 캐싱합니다.

JVM의 자동 최적화

JVM은 실행 중에 코드의 실행 패턴을 분석하여 최적의 성능을 발휘할 수 있도록 자동으로 최적화합니다.
개발자는 이러한 최적화 과정을 직접 제어할 필요가 없으며, JVM이 알아서 인터프리터와 JIT 컴파일러를 적절히 사용하여 최적의 성능을 보장합니다.

실행 엔진 동작 예시

1. Hello World! 출력 프로그램

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

이 간단한 프로그램은 System.out.println("Hello World!"); 한 줄만 실행하면 됩니다. 복잡한 계산이 없으므로 JIT 컴파일러가 개입할 필요가 없습니다.

프로그램은 인터프리터에 의해 한 줄씩 해석되고, 곧바로 "Hello, World!"를 출력합니다.

2. 반복 계산 프로그램

public class Calculation {
    public static void main(String[] args) {
        long sum = 0;
        for (int i = 0; i < 10000; i++) {
            sum += calculate(i);
        }
        System.out.println("Sum: " + sum);
    }

    public static int calculate(int number) {
        return number * 2;
    }
}

실행 과정

  • 초기 실행 단계 - 인터프리터 역할
    • 인터프리터가 바이트 코드를 한 줄씩 읽고 해석합니다.
    • main 메서드를 실행하고, for 루프를 반복하면서 calculate 메서드를 호출합니다.
    • 처음 몇 번의 calculate 메서드 호출은 인터프리터에 의해 해석되고 실행됩니다.

  • JIT 컴파일러 동작
    • JVM은 calculate 메서드가 자주 호출된다는 것을 감지합니다.
    • JIT 컴파일러가 calculate 메서드를 네이티브 코드로 컴파일합니다.
      • 네이티브 코드는 컴파일된 상태로 메모리에 저장됩니다.
    • 이후 호출되는 calculate 메서드는 인터프리팅 과정을 거치지 않고, 이미 컴파일된 네이티브 코드를 직접 실행합니다.

이 최적화 덕분에 calculate 메서드의 실행 속도가 크게 향상됩니다.

추가) 네이티브 코드 예시

.section .rodata
.LC0:
    .string "Hello, World!"
.section .text
.globl main
    .type main, @function
main:
    pushq   %rbp
    movq    %rsp, %rbp
    leaq    .LC0(%rip), %rdi
    call    puts
    movl    $0, %eax
    popq    %rbp
    ret


ref.
https://ssocoit.tistory.com/270#1._JVM%EC%9D%B4%EB%9E%80?
[Java] 컴파일 과정

0개의 댓글

관련 채용 정보