Compile(컴파일)에 대해 알아보자

Daniel·2023년 8월 9일
0

들어가기에 앞서 필자가 작성한 그림을 보고가자 낯이 익다면 그건 착각이다..?

런타임 환경 안 보라색 영역은 JVM을 의미합니다.

컴파일 프로세스

  1. 개발자가 자바 소스코드(.java)를 작성합니다.
  2. 자바 컴파일러가 소스코드(.java)파일을 읽어 바이트코드(.class)로 컴파일 합니다. 바이트코드(.class)파일은 아직 컴퓨터가 읽을 수 없는 JVM(자바 가상 머신)이 읽을 수 있는 코드입니다.
  3. 컴파일된 바이트 코드(.class)를 JVM의 클래스 로더(Class Loader)에게 전달합니다.
  4. 클래스 로더는 동적로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역(Runtime Data Area), 즉 JVM의 메모리에 올립니다.
  5. 실행엔진(Execution Engine)은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행합니다. 이 때 실행 엔진은 두 가지 방식으로 변경합니다.
    • 인터프리터 : 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행합니다. 하나하나의 실행은 빠르나, 전체적인 실행 속도가 느리다는 단점을 가집니다.
    • JIT컴파일러 : 인터프리터의 단점을 보완하기 위해 도입된 방식으로 바이트 코드 전체를 컴파일하여 바이너리 코드로 변경하고 이후에는 해당 메서드를 더이상 인터프리팅 하지 않고, 바이너리 코드로 직접 실행하는 방식입니다. 하나씩 인터프리팅하여 실행하는 것이 아니라 바이트 코드 전체가 컴파일된 바이너리 코드를 실행하는 것이기 때문에 전체적인 실행속도는 인터프리팅 방식보다 빠릅니다.

클래스 로더의 세부동작

컴파일된 Example.class 파일은 java 명령을 통해 JVM 위에서 실행될 수 있습니다.

$ java Example

위 명령을 실행하게 되면, Class Loader는 컴파일된 .class 파일을 JVM의 Runtime Data Areas(JVM의 메모리)로 올리게 됩니다.

Class Loader 는 다음과 같은 종류가 있고 각자 수행하는 역할이 다릅니다.

Bootstrap Class Loader

JVM 시작 시에 가장 먼저 실행되는 Class Loader 이며, 다른 Class Loader가 나머지 시스템에 필요한 클래스를 로드할 수 있게 최소한의 클래스를 로드하는 역할을 합니다.
Bootstrap Class Loader에 의해 로드되는 클래스는 java.lang.Object, Class, ClassLoader 등이 있습니다.
(JDK 1.8 까지는 $JAVA_HOME/jre/lib/rt.jar 에 있는 클래스를 로드하였지만, JDK 9 이상에선 런타임이 모듈화되고 클래스 로딩 개념이 많이 바뀌었다고 합니다.)

Extension Class Loader(JDK 9 이상: Platform Class Loader)

${JAVA_HOME}/jre/lib/ext 디렉토리나 환경변수 java.ext.dir로 지정된 디렉토리에 위치한 확장 클래스들을 로드합니다.
JDK 1.8에 탑재된 JavaScript Runtime Nashorn(JVM JavaScript 엔진)도 Extension Class Loader에 의해 로드 되게 됩니다.

Application Class Loader(JDK 9 이상: System Class Loader)

Application Class Loader(System Class Loader)는 자바 Classpath 에 위치한 클래스나 .jar 를 로드합니다.

User-Defined Class Loader

애플리케이션 개발자가 코드 상에서 직접 생성하여 사용하는 Class Loader입니다.

위 클래스 로더들은 그림과같이 서로 계층 구조(부모관계)를 이루고 있으며, 클래스를 로드할때 서로 위임하는 구조입니다.
쉽게 말하면, 클래스를 로드할 때, Bootstrap Class Loader에서부터 System Class Loader 방향으로 로드를 시도하며, Application Class Loader 에서까지 클래스 로드를 실패하게 된다면, ClassNotFoundException이 발생하게 됩니다.
또한, 하위 클래스 로더의 경우 상위 클래스 로더가 로드한 클래스를 알 수 있으나, 상위 클래스 로더는 하위 클래스 로더가 로드한 클래스를 알지 못하고(가시범위 원칙),
상위 클래스 로더에서 로드한 클래스를 하위 클래스 로더가 또 다시 로드하지 않습니다. (유일성 원칙)

Class Loader는 다음과 같은 역할을 순서대로 수행하여, 바이트 코드가 실행될 수 있는 환경을 갖춥니다.

Loading -> Linking[Verify + Prepare + Resolving] -> Initialization

  • Verify(검증)
    컴파일 시 생성된 바이트 코드가 JAVA Language Specification 및 JVM Specificaition 를 준수하는지 검증하고 아니면 verification error를 발생시킵니다. 검사 과정이 까다롭기 때문에 클래스 로드 시 가장 시간이 많이 걸리는 작업입니다.

  • Prepare(준비)
    클래스에 정의된 필드, 메소드, 인터페이스 등을 메모리에 할당하여 데이터 구조를 준비합니다.

  • Resolving(분석)
    클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경합니다. C와 C++ 등 언어에서는 참조(Call by Reference)를 할 때, 실제 물리적인 메모리 주소를 참조하지만, 자바에 경우에는 JVM에 실제 물리적인 메모리 주소와 참조하는 객체의 이름이 매핑 되어 있어서(클래스, 필드, 메서드 등의 실제 메모리 주소 값들은 상수 풀에 저장 됨), 참조를 하게 될 때 메모리 주소 값이 아닌 객체의 이름 값을 참조하게 됩니다. 그래서 보통 객체를 로그로 찍게 되면 메모리 주소가 아닌 객체 이름이 나오게 됩니다. 이를 심볼릭 레퍼런스라고 하는데, 컴파일 된 Class 파일이 JVM에 올라오게 되면, 심볼릭 레퍼런스는 그에 맞는 물리적인 메모리 주소와 연결되며, 분석 단계에서 실제 메모리 주소로 변경됩니다.

  • Initialization(초기화)
    클래스(static) 변수를 설정한 값으로 초기화합니다.


Runtime Data Areas

Class Loader에 의해서 시스템이 실행되는데 필요한 클래스들은 차례대로 JVM의 Runtime Data Areas(JVM 메모리)에 올려지게 됩니다.

PC Register

Program Counter라고도 불리는 영역으로 쓰레드가 생성될 때 생기며, 각 스레드 별로 하나씩 존재합니다. 현재 쓰레드가 어떤 명령을 실행할 차례인지에 대한 정보(명령어 주소)가 저장되어 있습니다.

Method Area

대표적으로 static 변수와 클래스에 대한 정보(필드, 메서드 데이터)가 저장되는 공간으로 모든 쓰레드가 이를 공유하게 됩니다. 따라서 Thread-safe 하지 않습니다.
메서드 영역에 저장된 데이터는 프로세스 종료까지 메모리에 저장되어 있기에, 오라클 JVM(HotSpot JVM)에서는 Permanent Area 혹은 Permanet Generation(PermGen) 이라고도 부릅니다.
또한, 상수 풀(Runtime Constant Pool)도 이곳에 위치하고 있습니다.(JDK 1.8 이후)
상수풀은 클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역으로 상수 뿐만 아니라 메서드와 필드에 대한 모든 레퍼런스(물리적인 메모리 주소)를 저장하고 있으며, 런타임 시 JVM은 상수 풀에 저장된 실제 메모리 주소를 찾아 참조합니다.

JVM Stack

스택이라고 불리는 JVM Stack 은 각 쓰레드 별로 하나씩 생성되며, 메소드의 지역변수, 매개변수, 임시변수 등이 저장됩니다. 만약 프로그램 실행 시 메소드가 호출되면 스택에 쌓이게 되며(PUSH), 메소드가 종료되면 차례대로 스택에서 제거됩니다. (POP)
각 쓰레드 별로 독립적으로 생성되기 때문에 Thread-safe 문제가 없습니다.

Heap

모든 인스턴스(객체, 배열 ..)가 저장되는 공간으로 모든 쓰레드가 공유하게 되어 Thread-safe 하지 않습니다. 또한 GC(Garbage Collector)의 대상이 되는 영역입니다. JVM 성능에 대한 이야기를 할 때, 보통 이 Heap 영역을 어떻게 구성하는지에 대해 논의 하며, JVM 벤더마다 구성방식, 가비지 컬렉션 방식이 다릅니다. Heap 영역에 대해서는 후에 자세히 포스트를 작성

Native Method Stack

각 쓰레드 마다 생성되며 Native Method 에 대한 정보가 저장됩니다.


Execution Engine

Class Loader가 Runtime Data Areas로 클래스(바이트코드)를 올리면, 바이트코드들은 Execution Engine에 의해 실제 OS환경에 맞게 기계어로 변환되어 실행되게 됩니다.
Execution Engine 에는 자바 인터프리터와 JIT 컴파일러, Garbage Collector가 있습니다.

자바 인터프리터

인터프리터는 바이트 코드를 한줄 한줄 읽으면서 OS 환경에 맞게 기계어로 변경하는 역할을 합니다.
한줄 한줄 해석하기 때문에 실행 속도가 느립니다. (자바는 느린 언어라는 인식을 갖게 해줌)
이를 극복하기 위해 JIT 컴파일러가 나오게 되었습니다. (그래도 다른 컴파일 언어보다 느림)

JIT 컴파일러

JIT 컴파일러는 인터프리터를 사용했을 때의 속도를 보완하기 위해 나왔는데,
반복적으로 사용하는 코드를 미리 기계어로 바꾸어 캐싱해놓고, 다음 요청 시 캐싱한 기계어를 사용하게 됩니다.
모든 코드를 기계어(native code)로 바꾸면 좋겠지만, 바이트 코드를 기계어로 바꾸는 비용도 상당하기에 런타임 환경에서 모든 코드를 바꾸기엔 무리입니다.
따라서, JVM은 자주 사용하는 코드를 확인한 후 Hot Spot이라고 생각하면 바이트 코드를 컴파일하여 캐싱하게 됩니다.

Garbage Collector

Garbage Collector는 Heap 영역에 참조되지 않은 객체를 제거 하는 역할을 하는데, GC 까지 자세하게 기술하면 너무 방대해질 것 같아 후에 따로 포스트를 작성 하겠습니다.

Reference

https://www.happykoo.net/@happykoo/posts/242
https://jungeu1509.github.io/interview/JAVA_Compile/
https://yang-droid.tistory.com/48

Note

자료를 찾고 포스트를 작성하다보니..내가 예상했던 내용보다 좀 더 깊이 들어가버린 것 같다..ㅠ
두고두고 보고 수정하며 계속 공부해야겠다.

profile
응애 나 애기 개발자

0개의 댓글

관련 채용 정보