JVM은 바이트코드를 OS가 해석할 수 있는 기계어로 변환시켜 주는 기능을 수행한다. 그렇기 때문에 코틀린이나 스칼라처럼 자바 언어가 아니더라도 바이트코드로 컴파일 될 수있는 언어라면 JVM에서 실행이 가능한 것이다.
내 컴퓨터 운영체제에 맞는 JVM을 설치한다면, JVM을 통해 자바 프로그램을 실행할 수 있다.
Class Loader
:런타임 시점에 .class(=바이트코드)를 JVM(=Runtime Data Area)에 에 할당한다
Execution Engine
:메모리에 할당된 바이트코드를 컴퓨터가 읽을 수 있는 기계어로 해석한다. 해석 방식에는 interpreter와 캐싱을 사용한 JIT컴파일러 방식이 존재한다.
interpreter
: line by line 방식으로 한 줄 씩 해석한다.
JIT 컴파일러(Just-In-Time compiler)
: 동적 번역(dynamic translation)이라고도 불리는 이 기법은 프로그램의 실행 속도를 향상시킨다. 프로그램이 실행 중인 런타임에 실제 기계어로 변환해 주는 컴파일러를 의미합니다. 즉, JIT 컴파일러는 자바 컴파일러가 생성한 자바 바이트 코드를 런타임에 바로 기계어로 변환하는 데 사용합니다.
interpreter 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고, 그 다음부터는 네이티브 코드로 직접 실행하는 방식이다. 네이티브 코드는 캐싱되기 때문에 한 번 컴파일된 코드는 재사용이 가능하여 빠르게 실행할 수 있다. 하지만 무조건 JIT컴파일이 수행되는 것은 아니다. 해당 메서드가 얼마나 자주 수행되는지 체크하고, 일정 정도를 넘을 때에만 컴파일을 수행한다.
GC
: 가비지 컬렉터, 참조되지 않는 인스턴스들을 제거한다.
Runtime Data Area
:JVM이 OS로부터 할당받는 메모리 공간이다.
PC Register(Thread 마다 존재)
: 현재 수행 중인 JVM 명령어의 주소를 가진다.stack(Thread 마다 존재)
: Thread가 메서드를 호출할 때, 메서드의 정보(매개 변수, 지역 변수, 리턴 값)가 Frame 이라는 단위로 JVM stack에 저장된다. 그리고 메서드 호출이 종료될 때 스택에서 프레임이 제거된다.Native Method stack(Thread 마다 존재)
: Java 외의 언어로 작성된 메서드들을 위한 StackHeap(모든 Thread 공유)
: 인스턴스들이 저장되며 GC의 관리 대상이된다.Method Area(모든 Thread 공유)
: Class Loader가 저장한 Class의 정보(클래스인지 인터페이스인지, 메서드 이름, 메서드 코드...)를 분류하여 저장한다. 상수를 저장하는 Runtime Constant Pool 도 존재한다. Method Area 는 클래스 데이터를 위한 공간이라면 Heap 은 인스턴스를 위한 공간이다. 마찬가지로 GC의 관리 대상이 된다. 컴파일(Compile)은 주어진 언어로 작성된 컴퓨터 프로그램을 다른 언어의 동등한 프로그램으로 변환하는 프로세스이다.
.java파일을 JVM이 사용할 수 있는 .class(=바이트코드)파일로 컴파일하는 과정이다.
Test.java
public class Test {
public static void main(String[] args) {
System.out.println("Hello Test");
}
}
javac
C:\Users\..\Desktop\cmd>javac Test.java
JDK에 속한 자바 컴파일러인 javac를 사용하여 .java 파일을 바이트코드인 .class로 컴파일한다. 이제 JVM이 해석할 수 있는 언어로 변경된 것이다.
java
C:\Users\..\Desktop\cmd>java Test
Hello Test
실행과정 ->
1. JVM의 Class Loader를 통해 Test.class 파일을 JVM으로 로딩
2. JVM의 Execution engine이 인터프리터와 JIT를 사용하여 바이트코드를 기계어로 변환한다.
3. 변환된 기계어들은 Runtime Data Area에 배치되어 명령을 수행한다.
javap -c
C:\Users\박형진\Desktop\cmd>javap -c Test.class
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Hello Test
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
.class 파일을 역어셈블하여 인간이 이해할 수 있는 바이트코드로 출력하는 명령어이다. Opcode는 1byte의 크기를 가지기 때문에 바이트코드라고 불린다. 바이트코드는 2^8개의 명령어를 가질 수 있다.
기계어(=바이너리코드)와 바이트코드 둘 다 이진 코드로 작성된다. 하지만 기계어는 OS가 읽을 수 있는 언어이고 바이트코드는 JVM이 읽을 수 있는 언어이다. JVM은 바이트코드를 바이너리코드로 변환시켜주는 역할을 수행한다.