JVM 구조와 JAVA의 동작 원리

.·2022년 4월 10일
12
post-custom-banner

개요

자바 가상 머신은 자바 플랫폼의 초석이다. JAVA는 자바 가상 머신 위에서 실행 되며 JAVA의 동작 방식을 이해하기 위해서는 자바 가상 머신에 대한 이해가 선행 되어야 한다. 이번 글에서는 JVM의 구조, JAVA의 동작 방식을 살펴보고 정리해보겠다. 해당 글은 JAVA 11을 기준으로 JVM Specification, Oracle의 HotSpot JVM, 해당 레퍼런스를 토대로 작성된 블로그 글들을 바탕으로 작성되었다.

JDK, JRE, JVM

JDK

  • 자바 개발 환경으로 자바 어플리케이션을 개발하기 위해 필요한 도구를 제공한다.
  • 자바 언어를 바이트 코드로 컴파일 해주는 자바 컴파일러(javac), 자바 클래스 파일을 해석해주는 역 어셈블리어(javap) 등이 있다.
  • 자세한 내용은 Tools and Commands Reference 에서 확인이 가능하다.

JRE

  • JRE는 자바 실행 환경으로 JVM,자바 클래스 라이브러리, 기타 자바 어플리케이션 실행에 필요한 파일들을 포함한다.

JVM

JVM은 자바 가상 머신으로 자바 어플리케이션을 실행하는 가상 머신이다. 실제 컴퓨터로 부터 JAVA 어플리케이션 실행을 위한 메모리를 할당 받아 Runtime Data Area를 구성한다.

  • JVM은 인터프리터와 JIT 컴파일러를 통해 바이트 코드를 각 운영체제에 맞는 기계어로 해석시켜 실행시키고, 가비지 콜렉터를 통해 어플리케이션의 동적 메모리를 관리한다.
  • 자세한 내용은 The Java® Virtual Machine Specification와 • Java Virtual Machine Guide에서 확인 가능하다.

JVM의 구조

JVM 명세

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의 런타임 데이터 영역에 대해서 알아보겠다.

Runtime Data Area

자바 가상 머신은 프로그램 실행 중 다양한 런타임 데이터 영역을 사용한다. 런타임 데이터 영역은 모든 스레드들이 공유하는 영역과 스레드 별 할당되는 영역으로 구분된다. JVM을 시작하면 Heap 영역과 Method 영역이 생성되며 해당 영역들은 모든 스레드들이 공유한다. 각 스레드가 시작 될 때마다 스레드마다 PC Register, Stack, Navtive Method Stack이 생성되며 스레드가 종료될 때 사라진다. 마지막으로 모든 스레드들이 실행되고 종료되면 JVM이 종료되면서 Heap 영역과 Method 영역도 사라진다.

PC Register

  • PC Register는 자바 가상 머신이 현재 실행중인 명령어의 주소를 저장한다.

Stack

  • Stacke은 Frame이라는 자료구조를 저장한다.
  • Stack은 C 같이 전통적인 언어의 스택 구조와 비슷하다. - 지역 변수, 함수의 실행 결과를 저장하며 함수 호출과 반환을 담당한다.

Frame

  • 프레임은 데이터, 반환 값을 저장하는 자료구조이다.
  • 프레임은 함수가 호출될 때 생성되고 함수가 종료되면 사라진다.
  • 각 프레임은 지역 변수 배열, Operand Stack, Run Time Constant Pool에 대한 참조값을 지닌다.
  • 클래스파일의 함수에 대한 접근은 Runtime Constant Pool에 존재하는 심볼릭 링크를 통해 접근 가능하다.
  • 동적 할당은 코드 실행 시점에 심볼릭 링크를 해석해 고정된 주소값으로 변환시킨다.
  • 심볼릭 링크를 통한 late-binding은 객체 지향의 핵심이다.

Native Method Stack

  • 다른 언어로 작성된 코드를 실행할 때 사용되는 스택이다.

Heap

  • Heap 영역은 클래스의 인스터스들과 배열들이 저장되는 공간이다.
  • Heap 영역은 가비지 컬렉션라는 동적 메모리 관리 시스템에 의해 관리된다.
  • 힙 구성 방식, 가비지 컬렉션의 알고리즘은 JVM 구현체의 재량으로 자유롭게 구성된다.

Method

  • Method 영역은 런타임 상수 풀, 필드, 함수, 코드 등 클래스와 인터페이스의 구조가 저장되는 공간이다.

Runtime Constant Pool

  • 런타임 상수 풀은 클래스, 인터페이스 마다 존재하며 클래스 파일의 constant pool 테이블 영역이 저장되는 공간이다.
  • 각 클래스, 인터페이스의 전역 변수, 함수, 인스턴스 변수, 함수에 대한 심볼릭 링크가 존재한다.
  • 전역 변수와 전역 함수는 컴파일 시점에 할당되어 고정된 값으로 존재하며 인스턴스 변수와 인스턴스 함수는 심볼릭 링크로 존재하며 실행 시점에 고정된 주소로 변환된다.
  • 런타임 상수 풀은 클래스가 생성되어 Heap에 할당될 때 만들어지며 클래스가 삭제되면 사라진다.
  • 자세한 내용은 The Java® Virtual Machine Specification의 2장 The Structure of the Java
    Virtual Machine 확인 가능하다.

JAVA의 동작 원리

JAVA Execution Engine

자바 실행엔진이 어떻게 동작하는 지는 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 컴파일러이다. 인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식이다. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행되게 된다.

Class Loader

클래스 로더는 클래스 파일의 바이트 코드를 읽어 런타임 데이터 영역으로 가져온다. 클래스 로더는 부트스트랩 클래스 로더, 플랫폼 클래스 로더, 시스템 클래스 로더 3가지로 구분된다. 클래스 로더는 계층 구조를 가지고 있으며 시스템 클래스 로더는 플랫폼 클래스 로더를 부모로 가지고, 플랫폼 클래스 로더는 부트스트랩 클래스 로더를 부모로 가진다.

Class Loader의 종류

Bootstrap Class Loader

  • 네이티브 코드로 작성되었으며 JVM에 내장되어 있다.
  • JVM이 시작될 때 실행되며 java.lang package 처럼 JVM 실행에 필요한 클래스들을 로딩한다.

Platform Class Loader

  • java.lang.ClassLoader의 인스턴스로 Java SE platform API 등 자바에서 기본적으로 제공해주는 클래스를 로딩할 때 사용된다.
  • Bootstrap Class Loader를 부모로 가지고 있다.
  • JAVA 8까지는 Extension Class Loader로 불리다가 모듈 시스템이 도입되면서 Platform Class Loader로 명칭이 바뀌었다.

System Class Loader

  • java.lang.ClassLoader의 인스턴스로 유저가 작성한 클래스를 로딩할 때 사용된다.
  • ClassPath에 명시된 경로를 통해 클래스를 찾는다.
  • Platform Class Loader를 부모로 가지고 있다.
  • JAVA 8까지는 Applicaiton Class Loader로 불리다가 모듈 시스템이 도입되면서 Platform Class Loader로 명칭이 바뀌었다.

Class Loader의 특징

  • 위임 모델 : 클래스 로더는 기본적으로 위임 모델을 채택한다. 자신에게 클래스 로딩 요청이 들어오면 자신의 부모 클래스 로더에게 클래스 로딩 요청을 보내고 부모 클래스 로더가 클래스를 찾지못하면 그 후에 자신이 클래스를 탐색한다.
  • 계층 구조 : 상위 클래스 로더의 클래스는 하위 클래스에서 볼 수 있지만 그 반대는 불가능 하다. 이러한 계층 구조를 통해 클래스 로더의 책임은 분리하고 클래스 로더는 자신이 책임지는 클래스를 로딩할 수 있다.
  • 자세한 내용은 How the JVM Locates, Loads, and Runs Libraries, Class ClassLoader 에서 확인할 수 있다.

Loading, Linking, Initializing

JVM은 동적으로 로드, 링크, 초기화 과정을 진행한다. 로딩은 특정 이름을 가진 클래스 또는 인터페이스의 바이트 코드를 찾은 후 클래스 또는 인터페이스를 생성하는 과정이다. JAVA 어플리케이션의 동작은 JVM을 시작한 후 특정 클래스를 런타임 데이터 영역으로 로딩한 후 로딩,링크,초기화 과정을 거쳐 최종적으로 특정 클래스의 public static method void main(String []) 함수를 실행하는 것이다. 해당 과정을 실행하면서 연쇄적으로 다른 클래스들을 로딩,링크,초기화한다.

JVM 시작

JVM이 시작되면 런타임 데이터 영역이 생성되고 그 안에 메소드, 힙 영역이 할당된다. JVM에 내장된 BootStrap Class Loader는 java.lang.package 처럼 JVM 실행에 필요한 클래스들을 메소드 영역으로 로딩한다. System Class Loader를 통해 실행한 클래스를 메소드 영역으로 로딩한다.

로딩

클래스 또는 인터페이스의 생성은 해당 클래스의 필드, 메소드, 런타임 상수 풀 등 클래스가 가지고 있는 바이트코드를 찾은 후 JVM의 메소드 영역에 구성하는 것을 의미한다. 클래스 로더를 통해 로딩을 진행하며 A 클래스를 로딩했을 때 A 클래스의 부모 클래스가 존재할 경우 먼저 부모 클래스를 로딩한다.

링크

링크는 검증(verification), 준비(Prepare), 분석(Resolution) 3가지 과정으로 이루어져 있다.

  • 검증 : 로딩된 바이트 코드가 JVM 명세를 따르고 있는지 검증하는 과정
  • 준비 : 정적 필드를 각 유형의 기본값으로 초기화하는 과정 - Int type은 0으로, reference type은 null로 초기화 된다.
  • 분석 : 클래스의 런타임 상수 풀 안에 있는 Symbolic Reference를 고정된 주소 값으로 바꾸는 과정

검증, 준비, 분석 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 Specification5.4.5 Method Overriding에서 확인할 수 있다.

초기화

클래스 초기화 함수를 실행한다. 클래스에 작성된 static 초기화 함수를 모두 합쳐 한꺼번에 실행한다. 초기화 과정은 로딩-검증-준비 과정이 모두 끝났을 때 한번만 실행된다.

JVM 종료

일부 스레드가 Runtime 클래스의 종료 메서드나 중지 메서드, 클래스 시스템의 종료 메서드를 호출하면 JVM 종료 또는 중지 작업이 Security Manager에 의해 허용된다.

정리

  • JDK에 있는 자바 컴파일러를 통해 java 파일을 바이트 코드(class 파일)로 만들고, JRE에서 바이트 코드를 실행시키면 JVM이 시작되면서 JVM 위에서 바이트 코드가 기계어로 해석되어 실행된다.
  • JVM의 명세를 따르는 가상 머신은 모두 JVM이다. 대표적으로 Oracle의 Hotspot이 존재한다.
  • JVM의 런타임 데이터 영역에는 모든 스레드들이 공유하는 Heap, Method 영역, 각 스레드 마다 존재하는 Stack, PC Register, Native Method Stack이 존재한다.
  • HotSpot의 JAVA 실행 엔진은 일반적으로 한줄 씩 바이트 코드를 읽어 인터프리터를 통해 기계어로 번역하며 자주 사용되는 바이트 코드는 JIT 컴파일러를 통해 캐시에 미리 컴파일 하는 방식으로 실행 엔진을 최적화 시킨다.
  • 클래스 로더에는 BootStrap, Platform, System 클래스 로더가 존재하며 각 클래스 로더들은 위임 모델과 계층 구조를 지닌다.
  • JVM은 동적으로 로딩,링크,초기화 과정을 진행하며 java 어플리케이션의 실행은 특정 클래스를 로딩, 링크, 초기화 과정을 거친 후 해당 클래스의 main method를 실행하는 것을 의미한다.
  • Java의 동적 바인딩은 실행 시점에 클래스의 런타임 상수 풀에 있는 Symbolic Referenc를 고정된 주소 값으로 바꾸는 것이며 이 때 고정된 주소 값을 선택하는 기준은 스택 위에 올라와 있는 객체의 타입이다

Reference

profile
지금부터 공부하고 개발한것들을 꾸준하게 기록하자.
post-custom-banner

2개의 댓글

comment-user-thumbnail
2022년 6월 25일

정리가 매우 잘되어있네요 감사합니다.

답글 달기
comment-user-thumbnail
2022년 12월 2일

좋은 글 감사합니다.

답글 달기