1. JVM이란 무엇인가?

kang·2021년 11월 6일
0

Spring Study

목록 보기
1/2

JVM이란 무엇인가?

Sun Microsystems(현 Oracle 사에 인수됨)에서 처음 개발한 가상머신 규격 혹은 사양으로 'WORA'
(Write Once, Run Anywhere), 즉 작성된 코드 하나로 다양한 기계, 운영체제에서 돌아가는 플랫품을 만들기 위해 만들어졌다. (현재는 'JCP'(Java Community Process)라는 여러 회사가 모인 위원회에서 표준을 정의한다.)
이것이 가능한 이유는 Java 코드를 작성해서 컴파일하면 기계어로 컴파일되는 것이 아닌 중간 언어인 'Java Bytecode'란 기계어와 가까운 언어로 번역된다. CPU 아키텍쳐 종속적인 기계어와 달리 'Java Bytecode'는 해당 플랫폼에 맞는 'JVM'이 존재하면 기계어로 해석해서 다양한 플랫폼에서 거의 동일하게 작동할 수 있는 것이다. 이런 JVM 플랫폼이 커지면서 'Java Bytecode'로 번역해서 같이 쓰는 언어들이 탄생했다. 대표적으로 'Kotlin', 'Scala'이다.

JVM은 사양이므로 다양한 곳에서 만든 구현체들이 존재한다. 그리고 'Oracle'의 'TCK'(Technology Compatibility Kit)라고 엄격한 테스트를 통과해야 'JVM'이라고 말할 수 있다.
대표적인 구현체만 확인해볼려고 한다.

  1. 'Oracle'의 상용 JVM JavaSE
  2. 오픈소스인 OpenJDK

첫번째로 'Oracle'의 'JavaSE'로 자바 초창기부터 존재한 유서깊은 'JVM'이다. 현재는 유료 상태이므로 실제 제품을 개발할 때부터 서버나 제품의 작동까지 회사에 비용을 지불해야하므로 사용할 때 부담이 될 수 있다.

두번째로는 'OpenJDK'로 이름에서부터 오픈소스라고 이야기하는 이름이다. 2006년부터, Sun 시절부터 Oracle 인수 이후에도 오픈소스로 개발되어 왔다. 개방된 프로젝트인 만큼 Oracle 뿐만 아니라 다양한 회사들이 참여 중이다.

JVM 구성 요소 및 동작 원리

'JVM'의 구성요소는 크게 3가지로 나눠집니다.

  1. Classloader
  2. Runtime Data Area
  3. Execution Engine

ClassLoader

'ClassLoader'는 'Java'의 특징인 클래스 단위로 구동되는 특징을 구현하는 기능인 동적 클래스 로딩을 구현하는 구성요소이다. 이것은 3단계로 나눠집니다.

  1. Loading
  2. Linking
  3. Initialization

이 모든 과정은 컴퓨터에서 프로그램이 'RAM'에 적재되고 실행되는 과정과 많은 부분 유사하다.

첫번째 Loading 단계에서는 클래스를 'RAM'에 적재하는 과정이다.

  1. ClassLoader 초기화 - 초기 classpath에서 클래스들을 적재합니다. 거의 rt.jar 밖에 없습니다.
  2. Extension ClassLoader - 확장 폴더에 존재하는 클래스들을 적재합니다.
  3. Application ClassLoader - 'Application'단의 'classpath'와 환경변수로 지정된 주소 등에서 클래스를 적재합니다.

이런 'ClassLoader'의 모든 과정은 위임-계층(Delegate-Hierarchy) 알고리즘으로 처리된다. 3 -> 2 -> 1 순으로, 즉 역순으로 원하는 클래스를 찾을 때까지 거슬러 올라갑니다.
이제 이렇게 'RAM'에 적재된 클래스들을 확인하고 Static 변수들을 기본값으로 초기화하고 모든 Symbolic 메모리 참조들을 실제 메모리 참조로 변경하는 작업을 하는 과정이 'Linking'입니다. 이것또한 3단계로 나눌 수 있다.

  1. 확인 - 'Java Bytecode'가 정확한 규격인지 확인하고 아니면 오류를 발생한다.
  2. 준비 - 클래스의 모든 Static 변수들을 메모리에 활당하고 기본값으로 초기화한다.
  3. 환원 - 클래스의 모든 Symbolic 메모리 참조들을 'Method Area'(Runtime Data Area에 존재한다)의 메모리 참조들로 교체한다.

클래스 로딩의 마지막 단계로 'Initialization'(초기화) 단계로 간단히 서술하면 'Linking' 단계에서 기본값으로 초기화된 Static 변수를 지정된 값이 있으면 변경하고 'Static Block'이 존재하면 실행한다.

Runtime Data Area

'JVM Runtime'에서 코드 실행에 필요한 데이터를 의미합니다. 총 5가지로 구분되어 있습니다.

  1. Method Area
  2. Heap Area
  3. Stack Area
  4. PC Registers
  5. Native Method Stacks

Method Area

모든 클래스 단위의 데이터들과 클래스에 존재하는 Static 변수들이 여기에 존재합니다. JVM 내에서 단 하나만 존재하고 공유합니다.

Heap Area

모든 객체와 instance 변수, 배열이 여기에 저장합니다. 'Method Area'와 같이 단 하나만 존재하고 공유합니다. 다만 내용이 변경될 가능성이 있으므로 'Thread-Safe'하지 않습니다.

Stack Area

모든 JVM 상의 쓰레드는 분리된 'Runtime Stack'이 만들어집니다. 모든 메소드 호출 때마다 'Stack Frame'이라는 단위로 스택 메모리에 추가됩니다. 쓰레드마다 분리해서 존재하기 때문에 공유 자원이 아니다.

Stack Frame 구조
  1. Local Variable Array - 지역 변수들의 수와 실제 값들이 여기에 저장됩니다.
  2. Operand Stack - 중간 과정이 필요하면 이 영역은 해당 작업을 수행하기 위한 런타임 작업 공간이 됩니다.
  3. Frame Data - 메소드에 연관된 모든 'Symbol'이 저장됩니다. 'exception'(예외)를 예시로 들면 'catch' 구문이 이 영역에서 관리됩니다.

PC Registers

쓰레드 별로 존재하며 독립적으로 존재합니다. 실제 'CPU'의 'PC Register' 같이 현재 실행 중인 명령어의 주소를 가리키고 있습니다. 또한 다음에 실행될 명령어로 변경됩니다.

Native Method Stack

쓰레드 별로 존재하며 CPU 아키텍쳐에 맞게 컴파일된 함수를 실행할 때 필요한 정보들을 가지고 있다.

Execution Engine

'Java Bytecode'가 'Runtime Data Area'에 존재한다는 것을 알 수 있습니다. 'Java Bytecode'는 실행할려면 기계어로 번역해주는 작업이 필요합니다. 또한 'JVM'에서의 메모리 관리는 어디에서 하는지 서술되지 않았습니다. 이 2가지를 처리하고 실제로 코드가 실행되는 영역입니다. 이 부분은 3가지 나눠져있습니다.

  1. Interpreter - 'Interpreter'는 'bytecode'를 해석하는 것은 빠릅니다. 하지만 실행하는 것은 느립니다. 왜냐하면 메소드가 실행될 때마다 번역 작업이 필요하기 때문입니다. 이러한 단점을 해결하기 위해 'JIT Compiler'가 존재합니다.
  2. JIT Compiler - 아래에서 다뤄보도록 하겠습니다.
  3. Garbage Collector - 참조되지 않는 객체들을 수집하고 삭제합니다. 'Garbage Collector'(GC)는 system.gc()로 실행할 수 있습니다, 하지만 바로 실행되지 않고 다른 객체가 생성되어야지 'GC'가 작동합니다. (왠만해서 수동으로 작동시키지 마세요! 왠만하면 거의 모든 상황에서 수많은 엔지니어들이 최적해놓은 기본값이 최적의 성능을 보장합니다!)

기타 영역

JNI(Java Native Interface) - 'JNI'는 'Java Native Library'와 기계어로 이루어진 라이브러리와 소통할 때 'Execution Engine'에서 사용합니다.
Native Method Libraries: 'Execution Engine'에 필요한 기계어 라이브러리 몪음입니다.

자바 컴파일, 자바 런타임

자바에서의 컴파일은 고급언어에서 'Java Bytecode'라는 중간 언어로 번역하는 과정을 의미합니다.
자바 런타임은 'Java Bytecode'가 실행되는 환경, 즉 'JVM'을 의미합니다.

바이트코드란?

고급 언어와 기계어 사이의 중간 언어로 기계어처럼 바로 실행되지 않지만 기계, 운영체제에 호환되는 'JVM'이 있다면 어디든 거의 동일한 동작을 보장한다. (JNI 기반 라이브러리, 운영체제, CPU 아키텍처에 따른 한계로 동작이 있을 수 있으므로 테스트해봐야한다. Write Every, Test Anywhere)

JIT 컴파일러란?

인터프리터의 단점을 극복하기 위해 사용하는 방식입니다. 이 방식은 기존처럼 'Interpreter'로 번역하다 반복되는 코드가 있다고 하면 번역해놓고 호출 때 미리 번역된 기계어를 실행해서 실행 속도를 개선합니다. 'JVM'에서 'JIT 컴파일러'는 5가지 구성요소를 가지고 있습니다.

  1. Intermediate Code Generator - 중간 코드를 생성합니다.
  2. Code Optimizer - 위에서 생성한 중간 코드를 최적화합니다.
  3. Target Code Generator - 기계어 코드를 생성합니다.
  4. Profiler - 특별한 구성요소입니다. 'HotSpots'이란 부분들을 찾습니다. 예를들면 '이 메소드가 반복해서 실행될 것인가 아닌가?' 등의 다양한 기준점으로 최적화 지점을 찾아냅니다.

참고자료

The JVM Architecture Explained - DZone Java
Naver D2 - JVM Internal
"JVM이란 무엇인가" 자바 가상 머신 이해하기

profile
맨정신 아닌 리눅서

0개의 댓글