JVM(Java Virtual Machine)이란?
Java Virtual Machine, 즉 자바 가상 머신은 자바 바이트코드를 실행하고자 하는 모든 하드웨어에 WORA(Write Once Run Anywhere)을 구현할 수 있도록 설계된 소프트웨어이다. 가상머신이란 프로그램을 실행시키기 위한 물리적인 하드웨어를 소프트웨어적으로 구현한 것을 의미한다. JVM은 자바 언어로 작성된 코드가 특정 플랫폼에 종속되지 않고 모든 플랫폼에 적용될 수 있도록 클래스 로더를 통해 읽어 들인 클래스 파일을 실행시킨다. Java 언어는 JVM 덕분에 특정 OS에 종속적이지 않지만 JVM은 특정 플랫폼에 따라 호환되는 JVM을 맞춰 주어야 하기 때문에 플랫폼 종속적인 성격을 띈다.
JVM의 특징은 다음과 같다.
- 스택 기반의 가상 머신: 대표적인 컴퓨터 아키텍처인 인텔 x86 아키텍처나 ARM 아키텍처와 같은 하드웨어가 레지스터 기반으로 동작하는 데 비해 JVM은 스택 기반으로 동작한다.
- 심볼릭 레퍼런스: 기본 자료형(primitive data type)을 제외한 모든 타입(클래스와 인터페이스)을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스를 통해 참조한다.
- 가비지 컬렉션(garbage collection): 클래스 인스턴스는 사용자 코드에 의해 명시적으로 생성되고 가비지 컬렉션에 의해 자동으로 파괴된다.
- 기본 자료형을 명확하게 정의하여 플랫폼 독립성 보장: C/C++ 등의 전통적인 언어는 플랫폼에 따라 int 형의 크기가 변한다. JVM은 기본 자료형을 명확하게 정의하여 호환성을 유지하고 플랫폼 독립성을 보장한다.
- 네트워크 바이트 오더(network byte order): 자바 클래스 파일은 네트워크 바이트 오더를 사용한다. 인텔 x86 아키텍처가 사용하는 리틀 엔디안이나, RISC 계열 아키텍처가 주로 사용하는 빅 엔디안 사이에서 플랫폼 독립성을 유지하려면 고정된 바이트 오더를 유지해야 하므로 네트워크 전송 시에 사용하는 바이트 오더인 네트워크 바이트 오더를 사용한다. 네트워크 바이트 오더는 빅 엔디안이다.
- 출처 - JVM Internal
JVM은 일종의 스팩으로 단 하나의 머신만을 지칭하는 용어는 아니다. 만약 JVM 명세(The Java Virtual Machine Specification)을 따르기만 한다면 어떤 벤더든 JVM을 개발해 제공할 수 있다. 보다 자세한 정보를 얻고 싶다면 "JVM이란 무엇인가" 자바 가상 머신 이해하기 게시물을 참고해보자. 이 부분은 필자가 완전히 이해한 부분이 아니라 게시글 링크만 가볍게 남겨둔다...
JVM의 구성
Class Loader
JVM 내로 클래스 파일을 불러 와 링크를 통해 배치하는 작업을 수행하는 모듈이다. 자바는 동적 로드, 즉 컴파일 타임이 아니라 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크하는 특징이 있는데, 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더이다.
클래스 로더의 특징은 다음과 같다.
- 계층 구조: 클래스 로더끼리 부모-자식 관계를 이루어 계층 구조로 생성된다. 최상위 클래스 로더는 부트스트랩 클래스 로더(Bootstrap Class Loader)이다.
- 위임 모델: 계층 구조를 바탕으로 클래스 로더끼리 로드를 위임하는 구조로 동작한다. 클래스를 로드할 때 먼저 상위 클래스 로더를 확인하여 상위 클래스 로더에 있다면 해당 클래스를 사용하고, 없다면 로드를 요청받은 클래스 로더가 클래스를 로드한다.
- 가시성(visibility) 제한: 하위 클래스 로더는 상위 클래스 로더의 클래스를 찾을 수 있지만, 상위 클래스 로더는 하위 클래스 로더의 클래스를 찾을 수 없다.
- 언로드 불가: 클래스 로더는 클래스를 로드할 수는 있지만 언로드할 수는 없다. 언로드 대신, 현재 클래스 로더를 삭제하고 아예 새로운 클래스 로더를 생성하는 방법을 사용할 수 있다.
- 출처 - JVM Internal
클래스 로더의 계층구조는 총 4계층으로 최상위 계층부터 최하위 계층까지 부트스트랩 클래스 로더, 익스텐션 클래스 로더, 시스템 클래스 로더, 사용자 정의 클래스 로더라는 이름을 가지고 있다. 이 부분은 어려워서 다음에 클래스 로더를 조금 더 깊게 공부할 기회가 생기면 찾아보도록 하겠다. 클래스 로더에 대해 더 궁금하다면 위에 링크된 JVM Internal 게시글을 참고하길 바란다.
Execution Engine
클래스 로더로부터 불러 들인 클래스를 실행시키는 역할을 수행한다. 클래스 로더는 바이트 코드를 JVM 내의 런타임 데이터 영역에 배치하고 이를 실행 엔진이 실행하는 것이다. 바이트 코드의 각 명령어는 1바이트의 OpCode와 추가 피연산자로 이루어져 있는데 실행 엔진은 하나의 OpCode와 피연산자를 함께 수행한 후 다음 OpCode와 피연산자를 수행하는 방식으로 동작한다. 실행 엔진에 의해 실행되는 바이트 코드는 기계보다는 비교적 인간이 보기 편한 형태로 기술되어 있기 때문에 실행 엔진은 이를 기계가 실행할 수 있는 실제적인 형태로 변환한다.
실행 엔진이 바이트 코드를 변환하는 방식은 두 가지로 다음과 같다.
- 인터프리터: 바이트코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나씩 해석하고 실행하기 때문에 바이트코드 하나하나의 해석은 빠른 대신 인터프리팅 결과의 실행은 느리다는 단점을 가지고 있다. 흔히 얘기하는 인터프리터 언어의 단점을 그대로 가지는 것이다. 즉, 바이트코드라는 '언어'는 기본적으로 인터프리터 방식으로 동작한다.
- JIT(Just-In-Time) 컴파일러: 인터프리터의 단점을 보완하기 위해 도입된 것이 JIT 컴파일러이다. 인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식이다. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행되게 된다.
- 출처 - JVM Internal
JIT 컴파일러가 컴파일하는 과정은 바이트코드를 하나 하나 인터프리팅하는 것보다 훨씬 오래 걸리기 때문에 만약 딱 한번만 실행되는 코드라면 JIT 컴파일을 수행하지 않고 인터프리팅하는 것이 훨씬 유리하다. 앞서 설명한 것과 마찬가지로 JVM은 하나의 제품이라기보다는 스펙에 가깝기 때문에 실행 엔진이 어떻게 동작해야 하는지에 대한 명세 또한 존재해야 하는데 JVM 명세에는 실행 엔진의 동작에 대해서는 규정되어 있지 않다. 이는 JVM이 메모리를 관리하는 GC도 마찬가지인데, 이를 설명하는 스펙 문서의 문구를 하나 가져와 봤다.
Implementation details that are not part of the Java Virtual Machine's specification would unnecessarily constrain the creativity of implementors.
해석해보면 "JVM 스펙에 명시되어 있지 않은 구현에 대한 상세 사항은 구현자의 창의성을 제약하는 불필요한 제약사항일 수 있습니다." 라고 쓰여 있는 것을 알 수 있다.
Runtime Data Area
그림 상에 JVM Memory로 적혀 있는 영역으로 자바 애플리케이션이 실행될 때 사용되는 데이터가 적재되는 영역이다.
각각의 영역의 특징은 다음과 같다.
- Method area: 모든 스레드가 공유하는 메모리 영역이다. 메소드 영역은 클래스, 인터페이스, 메소드, 필드, static 변수 등이 바이트 코드 형태로 보관된다.
- Heap area: 모든 스레드가 공유하는 메모리 영역으로 new 키워드로 생성된 객체와 배열이 생성되는 영역이다. Garbage Collector는 힙 영역에 참조되지 않는 메모리를 확인하고 제거한다.
- JVM Language Stacks: 메소드 프레임들이 쌓이는 영역이다. 메소드 프레임은 간단하게 메소드라고 생각해도 된다. 스택 자료구조이기 때문에 후입선출의 형태로 실행되고, 각 스레드는 자신만의 고유한 JVM Language 스택을 가지고 있다.
- PC Register: 스레드가 처음 시작될 때 생성되고 스레드의 수만큼 존재한다. 스레드가 어떤 부분을 어떤 명령으로 실행해야 하는지에 대한 기록을 하는 부분으로 현재 수행 중인 JVM 명령의 주소를 갖는다.
- Native Method Stacks: 자바 프로그램이 컴파일되어 생성되는 바이트 코드가 아닌 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역이다. Native Method 스택은 자바 언어 이외의 언어로 작성된 코드를 실행할 때 쓰이는 메모리 영역이다.
'JAVA' 카테고리의 다른 글
[번역] cron4j quickstart (0) | 2022.04.01 |
---|---|
Java의 날짜/시간 API. Date와 Calendar는 왜 사용하면 안될까? (0) | 2022.03.03 |
JVM의 Garbage Collector 동작 원리 (0) | 2022.02.14 |
[Java] Array와 ArrayList의 차이점 (0) | 2022.02.11 |
스프링의 역사 | 스프링은 왜 탄생했는가 (2) | 2021.11.07 |