자바 가상 머신은 자바 플랫폼의 초석이다. JAVA는 자바 가상 머신 위에서 실행 되며 JAVA의 동작 방식을 이해하기 위해서는 자바 가상 머신에 대한 이해가 선행 되어야 한다. 이번 글에서는 JVM의 구조, JAVA의 동작 방식을 살펴보고 정리해보겠다. 해당 글은 JAVA 11을 기준으로 JVM Specification, Oracle의 HotSpot JVM, 해당 레퍼런스를 토대로 작성된 블로그 글들을 바탕으로 작성되었다.
JDK
JRE
JVM
JVM은 자바 가상 머신으로 자바 어플리케이션을 실행하는 가상 머신이다. 실제 컴퓨터로 부터 JAVA 어플리케이션 실행을 위한 메모리를 할당 받아 Runtime Data Area를 구성한다.
JVM 명세 (The Java® Virtual Machine Specification)를 따르기만 한다면 누구나 JVM을 개발하여 제공할 수 있다. 대표적으로 오라클의 핫스팟 JVM, IBM JVM 이외에도 다양한 JVM이 존재한다. JVM의 명세를 살펴보면 다음과 같은 말이 나온다.
To implement the Java Virtual Machine correctly, you need only be able to
read the class file format and correctly perform the operations specified therein. Implementation details that are not part of the Java Virtual Machine's specification
would unnecessarily constrain the creativity of implementors.
JVM의 실행시키기 위해서는 클래스 파일을 읽어서 지정된 작업을 올바르게 수행하기만 하면 된다. 명령 실행에 대한 구체적인 사항은 구현자의 창의력을 저해시킬 수 있기 때문에 JVM에 명시하지 않겠다.
For example, the memory layout of run-time data areas, the garbage-collection algorithm used, and any internal optimization of the Java Virtual Machine instructions (for example,
translating them into machine code) are left to the discretion of the implementor.
예를 들어 런타임 영역에 대한 메모리 배치, 가바지 컬렉션의 알고리즘, 바이트 코드를 기계어로 변환하는 방법들은 구현자의 재량으로 남겨두겠다.
JVM 명세는 모든 JVM이 필수적으로 지켜야하는 사항에 대해서만 명시하고 있으며 구체적인 구현 방법은 JVM마다 다르다. 이번 장에서는 JVM 명세를 바탕으로 JVM의 런타임 데이터 영역에 대해서 알아보겠다.
자바 가상 머신은 프로그램 실행 중 다양한 런타임 데이터 영역을 사용한다. 런타임 데이터 영역은 모든 스레드들이 공유하는 영역과 스레드 별 할당되는 영역으로 구분된다. JVM을 시작하면 Heap 영역과 Method 영역이 생성되며 해당 영역들은 모든 스레드들이 공유한다. 각 스레드가 시작 될 때마다 스레드마다 PC Register, Stack, Navtive Method Stack이 생성되며 스레드가 종료될 때 사라진다. 마지막으로 모든 스레드들이 실행되고 종료되면 JVM이 종료되면서 Heap 영역과 Method 영역도 사라진다.
자바 실행엔진이 어떻게 동작하는 지는 JVM 명세에 작성되어 있지 않다. 위에서도 볼 수 있듯이 “명령 실행에 대한 구체적인 사항은 구현자의 창의력을 저해시킬 수 있기 때문에 JVM에 명시하지 않겠다.” 라고만 적혀있다. 즉 자바 실행 엔진은 JVM마다 다른 것이다. 그렇다면 Oracle의 Hotspot은 어떻게 자바 명령을 실행하는지 간단하게 살펴보자
A standard interpreter is used to launch the applications.
일반적인 인터프리터가 어플리케이션을 시작하는데 사용된다.
When the application runs, the code is analyzed to detect performance bottlenecks, or hot spots. The Java HotSpot VM compiles the performance-critical portions of the code for a boost in performance, but does not compile the seldom-used code (most of the application).
어플리케이션이 동작할 때, 코드를 분석하여 bottleneck 또는 HotSpot을 탐지한다. HotSpot 가상머신은 성능 향상을 위해 코드의 성능에 중요한 부분을 컴파일하지만 거의 사용되지 않는 코드(대부분의 어플리케이션)는 컴파일하지 않는다.
The Java HotSpot VM uses the adaptive compiler to decide how to optimize compiled code with techniques such as inlining.
HotSpot 가상머신은 코드를 컴파일 하는 방법을 최적화 하기 위해 라인별 adaptive compiler를 사용한다.
즉 HotSpot 가상머신은 라인별로 바이트 코드를 읽어 기계어로 변환해 실행하며 기본적으로는 인터프리터를 통해 실행을 하지만 자주 등장하는 바이트 코드일 경우 JIT 컴파일러를 사용해 컴파일을 하는 방법을 통해 실행 방법을 최적화 시킨다. 자세한 내용은 Java Virtual Machine Guide 을 참고하자
인터프리터: 바이트코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나씩 해석하고 실행하기 때문에 바이트코드 하나하나의 해석은 빠른 대신 인터프리팅 결과의 실행은 느리다는 단점을 가지고 있다. 흔히 얘기하는 인터프리터 언어의 단점을 그대로 가지는 것이다. 즉, 바이트코드라는 '언어'는 기본적으로 인터프리터 방식으로 동작한다.
JIT(Just-In-Time) 컴파일러: 인터프리터의 단점을 보완하기 위해 도입된 것이 JIT 컴파일러이다. 인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식이다. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행되게 된다.
클래스 로더는 클래스 파일의 바이트 코드를 읽어 런타임 데이터 영역으로 가져온다. 클래스 로더는 부트스트랩 클래스 로더, 플랫폼 클래스 로더, 시스템 클래스 로더 3가지로 구분된다. 클래스 로더는 계층 구조를 가지고 있으며 시스템 클래스 로더는 플랫폼 클래스 로더를 부모로 가지고, 플랫폼 클래스 로더는 부트스트랩 클래스 로더를 부모로 가진다.
Bootstrap Class Loader
Platform Class Loader
System Class Loader
JVM은 동적으로 로드, 링크, 초기화 과정을 진행한다. 로딩은 특정 이름을 가진 클래스 또는 인터페이스의 바이트 코드를 찾은 후 클래스 또는 인터페이스를 생성하는 과정이다. JAVA 어플리케이션의 동작은 JVM을 시작한 후 특정 클래스를 런타임 데이터 영역으로 로딩한 후 로딩,링크,초기화 과정을 거쳐 최종적으로 특정 클래스의 public static method void main(String []) 함수를 실행하는 것이다. 해당 과정을 실행하면서 연쇄적으로 다른 클래스들을 로딩,링크,초기화한다.
JVM이 시작되면 런타임 데이터 영역이 생성되고 그 안에 메소드, 힙 영역이 할당된다. JVM에 내장된 BootStrap Class Loader는 java.lang.package 처럼 JVM 실행에 필요한 클래스들을 메소드 영역으로 로딩한다. System Class Loader를 통해 실행한 클래스를 메소드 영역으로 로딩한다.
클래스 또는 인터페이스의 생성은 해당 클래스의 필드, 메소드, 런타임 상수 풀 등 클래스가 가지고 있는 바이트코드를 찾은 후 JVM의 메소드 영역에 구성하는 것을 의미한다. 클래스 로더를 통해 로딩을 진행하며 A 클래스를 로딩했을 때 A 클래스의 부모 클래스가 존재할 경우 먼저 부모 클래스를 로딩한다.
링크는 검증(verification), 준비(Prepare), 분석(Resolution) 3가지 과정으로 이루어져 있다.
검증, 준비, 분석 3가지 과정을 거치면서 다른 클래스의 로딩을 추가적으로 요청할 수 있다. 이 때 분석 과정은 검증, 준비 과정과 같은 시간에 일어날 필요가 없다. 보통 Symbolic Reference를 고정된 주소 값으로 변환시키는 분석 과정은 해당 명령이 실행될 때 일어난다.
오버라이딩 된 함수는 실행 시점에 해당 함수를 호출하는 메세지와 함수가 분석과정을 통해 연결된다. 그렇다면 어떤 기준으로 함수를 선택하는 것일까?
During execution of an invokeinterface or invokevirtual instruction, a method is selected with respect to (i) the run-time type of the object on the stack, and (ii) a method that was previously resolved by the instruction.
invokeinterface 또는 invokevirtual 이라는 바이트 코드와 특정 메소드를 연결할 때 , 스택 최상단에 올라와 있는 객체의 타입에 따라 메소드를 결정한다. 즉 Java의 동적 바인딩은 실행 시점에 클래스의 런타임 상수 풀에 있는 Symbolic Referenc를 고정된 주소 값으로 바꾸는 것이며 이 때 고정된 주소 값을 선택하는 기준은 스택 위에 올라와 있는 객체의 타입이다. 자세한 내용은 The Java® Virtual Machine Specification의 5.4.5 Method Overriding에서 확인할 수 있다.
클래스 초기화 함수를 실행한다. 클래스에 작성된 static 초기화 함수를 모두 합쳐 한꺼번에 실행한다. 초기화 과정은 로딩-검증-준비 과정이 모두 끝났을 때 한번만 실행된다.
일부 스레드가 Runtime 클래스의 종료 메서드나 중지 메서드, 클래스 시스템의 종료 메서드를 호출하면 JVM 종료 또는 중지 작업이 Security Manager에 의해 허용된다.
정리가 매우 잘되어있네요 감사합니다.