자바 실행 방식
컴파일 하는 방법
1. 개발자가 자바 소스코드(.java)를 작성한다.
2. 자바 컴파일러(Java Compiler)가 자바 소스파일을 컴파일하여 자바 바이트 코드 생성
3. 컴파일된 바이트 코드를 JVM의 클래스로더(Class Loader)에게 전달
4. 클래스 로더는 동적로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역(Runtime Data area), 즉 JVM의 메모리에 올린다.
클래스 로더 세부 동작 a. 로드 : 클래스 파일을 가져와서 JVM의 메모리에 로드한다. b. 검증 : 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 구성되어 있는지 검사한다. c. 준비 : 클래스가 필요로 하는 메모리를 할당(필드, 메서드, 인터페이스 등등) d. 분석 : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다. e. 초기화 : 클래스 변수들은 적절한 값으로 초기화한다.(static 필드)
5. 실행엔진(Execution Engine)이 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행
실행하는 방법
JDK 설치
설치된 디렉토리의 bin 디렉토리를 path에 추가
자바 컴파일러를 사용해 소스코드로부터 클래스 파일을 생성
자바 인터프리터로 클래스 파일 실행
컴파일러와 인터프리터
컴파일러 방식
프로그래밍 언어로 작성된 코드를 타겟 언어로 변환(번역)하는 프로그램
주로 High-Level 언어를 Low-Level 언어(assembly, object code, machine code 등)로 변환
인터프리터 방식
읽은 코드 및 해당 명령을 직접 분석/실행하는 프로그램
세가지 방식을 사용할 수 있다.
코드 구문을 분석하고 동작하는 것을 직접 모두 수행
코드를 object code로 변환하고 즉시 실행
컴파일러에 의해 생성된 바이트 코드를 명시적으로 실행
자바는 인터프리터의 두번째, 세번째 방식을 혼합해 사용하고 있으며, 컴파일 언어로 분류된다.
Java 실행을 위한 준비
플랫폼에 의존적이지 않게, 그리고 효율적으로 컴파일 하기 위해 자바 컴파일러(javac)와 JVM으로 분리한다.
자바 컴파일러는 소스 코드를 바이트코드로 변환하는 역할을 담당한다.
JVM에서는 클래스 로더가 로딩되어 실행에 필요한 것을 JVM 내부에 저장하고, 변환된 바이트코드를 실행 엔진이 해석하고 실행한다.
자바 컴파일러는 JVM 요소가 아닌 JDK의 일부이다.
바이트코드와 코드캐시
언어의 수준
Machine code
CPU를 제어하는 기계어로 된 명령 코드
각 명령 코드는 CPU 레지스터 또는 메모리에 있는 데이터에 대한 로딩, 저장, 연산 등 수행 일반 프로그래머가 액세스할 수 없는 특정 CPU 내부 코드
Binary code
두 개의 기호(일반적으로 0, 1)를 사용해 텍스트 또는 프로세스 명령을 나타내는 코드
기계어는 binary code로 구성되어있다.
Object code
일반적으로 컴파일러에 의해 생성된 중간 언어로 작성된 명령 코드
object code는 Binary code이지만 기계어는 아니다.
코드 캐시
JVM이 Java 바이트코드를 컴파일한 네이티브 코드를 저장하는 메모리 영역
실행 가능한 네이티브 코드의 블록을
nmethod
라고 함미할당된 영역에 할당되며 힙(자료구조) 형태로 JIT 컴파일러가 코드 캐시 영역을 가장 많이 사용하게 됨
AOT 컴파일러
Ahead of Time - 실행 전 바이트코드를 전부 컴파일해두는 방식
Java 바이트코드(.class 파일)를 AOT 컴파일러(jaotc) 통해 컴파일 하는 방식
이 기능을 수행하는 jaotc(Graal 컴파일러)가 JDK 17에서 제거되었지만 GraalVM을 별도로 추가하면 사용할 수 있다.
AOT 컴파일러를 구현한 것이 Graal Compiler이고, 이를 사용한 JVM이 GraalVM이다.
JVM 워밍업(부팅 시간)을 단축해 서버 실행 속도를 단축시키는 것이 가장 큰 목적
서버 실행 속도가 빠르면 클라우드 환경에 컨테이너 이미지를 배포할 때 빠르게 떠서 요청량을 감당할 수 있으므로 적합하다.
플랫폼에 의존적이고, 런타임에 알 수 있는 정보를 획득할 수 없으므로 이부분에서는 최적화가 불가능하다.
JIT 컴파일러
Just In Time 컴파일러라는 뜻으로, 바이트코드(컴파일된 자바 코드)를 하드웨어의 기계어로 바로 변환해준다.
hot code인지 확인한 후 JIT 컴파일러로 가거나 Interpreter로 간다.
C1과 C2 컴파일러 모드로 구성되어 있다.
Java 7 버전 이후에는 두 모드를 혼합하여 C1 모드로 웜업 후 C2로 전환 (7 이전 버전에선 하나만 골라야 함)
C1 (Client Compiler)
런타임에 바이트코드를 기계어로 컴파일
빠른 시작과 견고한 최적화가 필요한 앱에서 사용됨 (GUI 앱 등)
C2 (Server Compiler)
변환된 기계어를 분석하여 C1보다 오래 걸리지만 더욱 최적화된 네이티브 코드로 컴파일
오랫동안 실행을 하는 서버 앱 용도
플랫폼 확장성이 좋으며, 런타임 정보를 포함해 최적화 할 수 있다.
인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식으로 동작한다.
모든 코드를 인터프리팅하여 실행하는 것이 아니라 바이트 코드 전체가 컴파일된 바이너리 코드를 실행하는 것이기 때문에 전체적인 실행속도는 인터프리팅 방식보다 빠르다.
Last updated