목표
자바 기초를 다시 공부하면서 문득 내가 작성한 자바 코드가 어떻게 동작하는지 알고싶어졌다. 이번 글을 정리하면서 자바 코드가 바이트코드로 변환되는 과정과 JVM에서 바이트코드를 기계어로 바꾸는 과정을 완벽하게 이해하고자 한다.
Java 코드가 바이트코드로 변환되는 과정
Java는 사람이 읽기 쉬운 High-Level Language이기 때문에 기계가 읽을 수 없다. 따라서, 0과1로 이루어진 이진수의 코드로 변환해줘야하는데 그 과정을 간단하게 표현하자면 아래와 같다. 우리가 작성한 소스파일(.java)이 JDK의 java compiler를 통해 바이트코드파일(.class)로 변환되고 JRE의 JVM이 이를 OS가 이해할 수 있는 기계어로 변환한다.
좀 더 자세하게 들여다 보자. 우선, 소스코드를 기계어로 바꾸는 두가지 개념을 짚고 넘어가자면 런타임시 한줄 씩 읽어 기계어로 바꾸는 인터프리터 방식(Python, PHP 등)과 전체 소스코드를 읽어 한번에 기계어로 바꾸는 컴파일러 방식(C, C++ 등)이 있다.
Java의 경우 특이하게 런타임 시 컴파일러 방식, 인터프리터 방식을 모두 사용한다. 그럼 .java파일을 실행하면 어떤 일이 일어나는지 알아보자. 아래의 java 파일을 java compiler를 통해 bytecode(.class)로 변환해보자
Java는 소스코드를 바이트코드로 변환하는 과정에서는 컴파일러 방식을 사용한다. 때문에 소스코드파일(.java)이 java compiler를 통해 바이트코드파일(.class)로 변환된 모습이다. 기계어보다는 추상적이고 소스코드보다는 그렇지 않은게 어셈블러를 보는거 같다. 이제 이 파일을 JVM이 해석하여 기계어로 바꾸는 것이다. IntelliJ를 사용한다면 java파일을 선택하고 View -> Show Byte Code를 선택하면 확인 가능하다.
JVM에서 바이트코드를 기계어로 변환하는 과정
JVM에서 .class 바이트코드를 읽어 기계어로 변환하고 실행하는 과정은 아래와 같다. 컴파일된 .class 바이트파일을 Class Loader가 동적으로 메모리에 로딩하는데 그 작업은 크게 loading, linking, initializing으로 분류된다. 이후 Execution Engine에서 JVM Memory 영역에 있는 바이트코드를 읽어 네이티브 코드로 변환하고 마침내 OS가 이를 읽고 실행한다. Class Loader와 Execution Engine에 대한 설명은 아래를 참고하자.
✅ Class Loader
런타임에 .class 바이트코드를 필요할때마다 단위만큼 메모리에 로딩한다.
loading: JVM이 필요한 클래스 파일을 로드한다.
linking: 로드된 클래스의 verify, prepare, resolve 작업을 수행한다.
initializing: 클래스/정적 변수 등을 초기화한다.
✅ Execution Engine
JVM Memory 영역의 .class 바이트코드를 읽어 네이티브코드로 변환하고 실행한다.
Interpreter: JVM Memory에 로드된 바이트코드를 한줄씩 해석하고 실행한다.
JIT(Just-In-Time) Compiler: JVM Memory에 로드된 바이트코드의 전체를 읽어 한번에 해석하고 실행한다.
GC(Garbage Collector): JVM Memory를 관리하는 역할을 한다.
매번 Interpreter방식을 사용하게되면 미리 컴파일된 코드를 수행하는 것보다 수행 속도가 느리고 매번 Compiler 방식을 사용하게 되면 초기 속도가 매우 느려지게 된다. 따라서, JVM에서는 모든 코드를 초기 Interpreter를 이용해 변환하고 자주 호출되는 바이트코드는 JIT Compiler를 이용해 한번에 변환한다.
[ 참고자료 ]