자바 프로그램 실행 과정은 크게 컴파일 환경과 런타임 환경으로 구분된다.
이번 시간에는 컴파일 환경에 대해 알아보겠다.
컴파일 환경에서는 Java Compiler에 의해 Java Code(.java)
가 Byte Code(.class)
로 변환된다.
이번 시간에는 Java Compiler가 어떻가 Java Code(.java)
를 Byte Code(.class)
로 변환하는지 알아보겠다.
한번쯤 'Python으로 하면 실행 속도가 느리다' 라는 말을 들어본적이 있을 것이다. 왜 그럴까?
Python은 인터프리어 언어이기 때문이다.
ex. R, Python, Ruby
인터프리터 언어는 원시코드(개발자가 작성한 코드)를 기계언어로 변환하지 않고 한줄한줄 해석해 바로 명령어를 실행한다.
우리가 처음보는 새로운 언어로 적힌 문서를 만날 때 번역이 필요한 것처럼 컴퓨터도 마찬가지이다.
우리가 통역이 없으면 하나하나 사전을 찾아가며 의미를 찾는 것처럼 개발자가 작성한 코드가 기계언어로 변환되지 않았기 때문에 컴퓨터는 코드를 이해하는데 시간이 걸린다.
ex. C, C++
반면에 컴파일 언어는 원시코드(개발자가 작성한 코드)를 통째로 기계언어로 변환한 뒤, 기계(JVM 같은 가상 머신)에 넣어 기계어 코드를 실행한다. 원시코드를 기계언어로 변환하는 것은 우리에게 새로운 언어로 작성된 문서를 건내기 전에 미리 번역하여 건내주는 것이다. 이런 특징을 갖고 있는 컴파일 언어를 사용할 때 원시 코드를 기계언어로 바꾸는 역할이 바로 컴파일러이다.
다음과 같이 간단한 코드를 작성했다.
public class Test {
public static void main(String[] args){
System.out.println("Hello World!!");
}
}
java파일을 bytecode로 컴파일하기
javac
명령어를 통해 java 파일을 bytecode로 컴파일한다.
ls
명령어로 확인해보면 .class
파일이 생긴 것을 확인할 수 있다.
이제 Java의 Runtime Environment(JRE)에서 실행되기에 최적화된 형태가 된 것이다.
생성된 Test.class
파일을 확인해보면, 컴퓨터가 해석할 수 있는 bytecode로 변환된 것을 확인할 수 있다.
만약 잘못된 코드를 작성했다면 컴파일 과정에서 오류를 확인하여 알려준다.
다음 그림은 임의로 ;
를 작성하지 않아 오류를 발생한 상황이다. 컴파일 과정에서 오류를 확인했기 때문에 Test.class
파일도 생성되지 않을 것이다.
+) IntelliJ에서는 컴파일된 코드의 내부를 볼 수 있는 디컴파일러를 제공한다.
bytecode로 변환된 파일을 실행하기
bytecode로 변환된 파일은 JVM(Java Virtual Machine)을 위한 코드이다. JVM 위에서 코드가 동작하기 때문에 자바를 실행할 수 있는 모든 장치에서 코드가 실행된다.
java
명령어를 통해 컴파일된 파일을 실행할 수 있다.
이러한 컴파일 가정을 통해 자바 컴파일러의 두가지 큰 특징을 이해할 수 있다.
.java
파일에 오류가 있는지 검사한다. Bytecode로 변환된 파일은 런타임 환경에서 JVM에 의해 어떻게 실행되는 것일까?
ClassLoader는 Runtime 시, 흩어져 있는 바이트 코드로 변환된 .class
확장자를 가진 클래스 파일을 찾아 JVM의 메모리에 올려주는 역할을 한다. ClassLoader는 Loading 역할 뿐만 아니라 Linking, Initializing 역할도 한다.
자세한 부분은 Class Loader에 대해 알아보자를 참고하시기 바란다.
ClassLoader에 의해 JVM에 탑재된 클래스 파일들이 차지하는 영역을 말한다. JVM Memory에는 JVM 당 하나만 생성되는 Method Area, Heap와 각 Thread 별로 생성되는 Java Stack, PC Register, Native Method Stack이 존재한다.
자세한 부분은 자바 메모리 구조를 참고하시기 바란다.
Execution Engine은 할당받은 Class Loader를 이용해 메모리(Runtime Data Area)에 할당된 bytecode를 실행시킨다. Execution Engine은 기계가 읽을 수 있는 형태로 ByteCode를 변환해야 한다.
Execution Engine에서는 크게 3가지 구성요소가 실행된다.
Interpreter
인터프리터는 Bytecode를 기계가 이해할 수 있는 코드로 바꾼다. 이 덕분에 각 플랫폼에 맞는 인터프리터가 바이트 코드를 실행하여 플랫폼에 독립적이다라는 특징을 갖게 된다. 인터프리터는 Runtime 중에 바이트 코드를 한 라인씩 읽고 실행하기 때문에, 속도 문제가 발생한다.
JIT(Just In Time) Compiler
인터프리터는 메서드가 호출될 때마다 해석이 필요하기 때문에 속도가 느려질 수 있다. 이를 해결하기 위해 JIT 컴파일러가 사용된다.
JIT 컴파일러는 메서드가 호출되는 빈도를 파악하고 자주 사용되는 코드(ex. 반복문)에 대해 JVM이 기계 코드로 컴파일해둔다. 이미 컴파일되었기 때문에 Interpreter가 바로 사용할 수 있다.
Garbage Collector
RuntimeDataArea는 힙 영역에서 더 이상 참조되지 않는 객체를 모아서 정리한다.
Summary
- Java 프로그램의 실행 과정은 컴파일 환경과 런타임 환경으로 구분된다.
- 컴파일 환경에서는 Java 코드를 바이트 코드로 변환시킨다.
- 런타임 환경에서는 바이트 코드로 JVM에서 실행시킨다.
- ClassLoader는 Runtime 시, 흩어져 있는 바이트 코드로 변환된
.class
확장자를 가진 클래스 파일을 JVM 메모리에 올려준다.- Execution Engine은 Interpreter, JIT Compiler, Garbage Colector로 구성되어 바이트 코드를 기계어로 변환한다.
Reference