JVM(Java Virtual Machine)

📌 JVM이란?

자바로 개발한 프로그램을 컴파일해서 만들어지는 바이트코드를 실행시키는 가상머신이다.
스택 기반의 가상머신이다.
Java와 OS 사이에서 중개자 역할을 수행한다.
가장 중요한 메모리를 관리하고, GC(Gabage Collection)를 수행한다.

📌 HotSpot VM?

썬 마이크로시스템즈에서 개발했고, 현재는 오라클에 의해 유지 보수 및 배포가 이루어지고 있다.
정확한 명칭은 Java HotSpot Performance Engine
자바 성능을 개선하기 위해 JIT 컴파일러를 만들었는데 이 이름을 HotSpot으로 지었다.
1.3 버전부터 기본 VM으로 사용되어 왔고, 운영되고 있는 대부분의 시스템은 HotSpot 기반의 VM이다.

HotSpot VM은 어떻게 구성되어 있을까?

주요 컴포넌트는 VM(Virtual Machine) 런타임, JIT(Just In Time) 컴파일러, 메모리 관리자로 구성되어 있다.
VM 런타임은 JIT 컴파일러용 API와 가비지 컬렉터용 API를 제공한다. 그리고 JVM을 시작하는 런처, 스레드 관리, JNI 등도 VM 런타임에서 제공한다.

📌 JVM 세부적인 구조

1. 클래스 로더 시스템(Class Loader)

[로딩] 클래스 파일을 가져와서 JVM 메모리에 로드한다.
[링크] 레퍼런스를 연결하는 과정
[초기화] static 값들 초기화 및 변수에 할당

클래스 로딩의 세부적인 절차

로딩(loading) → 링크(linking) → 초기화(Initiallizing)
1.
주어진 클래스 이름으로 클래스 패스에 있는 바이너리로 된 자바 클래스를 찾는다.
2.
자바 클래스를 정의한다.
3.
해당 클래스를 나타내는 java.lang 패키지의 Class 클래스의 객체를 생성한다.
4.
링크 작업이 수행된다. 해당 단계에서 static 필드를 생성 및 초기화하고, 메서드 테이블을 할당한다.
5.
클래스의 초기화가 진행된다. 클래스의 static 블록과 static 필드가 가장 먼저 초기화 된다. (부모 클래스가 먼저 초기화 된다)
클래스가 로딩될 때 발생하는 에러

2. 메모리(Runtime Data Area)

PC(Program Counter) 레지스터

스레드가 시작될 때 생성되며, 각각 하나씩 존재한다.
현재 수행 중인 JVM 명령의 주소를 가진다. 즉, JVM의 인스트럭션 주소를 보관한다.

JVM 스택 영역

스레드가 시작될 때 생성되며, 각각 하나씩 존재한다.
스텍 프레임이라는 구조체를 저장하는 곳이며, 오직 추가(push)하고 제거하는(pop) 동작만 수행한다.
ex) 에러 메시지에 메서드 정보를 보여주는데 각 라인은 하나의 스택 프레임을 표현한다.
💡
스택 프레임(Stack Frame) - 스텍 프레임은 메서드가 수행되면 생성되고, 종료되면 제거된다. - 지역 변수와 임시 결과, 메서드 수행과 리턴에 관련된 정보들이 포함된다. - 각 스텍 프레임은 지역 변수 배열(Local Variable Array), 피연산자 스택(Operand Stack), 런타임 상수 풀에 대한 레퍼런스를 가진다. - 지역 변수 배열: 0부터 시작하는 인덱스를 가진 배열이다. 0은 메서드가 속한 클래스 인스턴스의 this 레퍼런스이고, 1부터는 메서드에 전달된 파라미터들이 저장되며, 메서드 파라미터 이후에는 메서드의 지역 변수들이 저장된다. - 피연산자 스택: 메서드의 실제 작업 공간이다. 각 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하고, 다른 메서드 호출 결과를 추가하거나(push) 꺼낸다(pop). 피연산자 스택 공간이 얼마나 필요한지는 컴파일할 때 결정할 수 있으므로, 피연산자 스택의 크기도 컴파일 시에 결정된다.

네이티브 메소드 스택

자바 외의 언어로 작성된 네이티브 코드를 위한 스택이다.
즉, JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 C 스택이나 C++ 스택이 생성된다.

메소드 영역

JVM이 시작될때 생성되며, 모든 스레드가 공유하는 영역이다.
메서드 영역은 클래스 수준의 정보들을 저장한다.
런타임 상수 풀 : 자바 클래스 파일에 있는 contant_pool 은 실제 상수값도 포함될 수 있고, 실행 시 변하게 되는 필드 참조 정보도 포함된다.
필드 정보에는 메서드 데이터, 메서드와 생성자 코드가 있다.

힙 영역

인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션 대상이다.
JVM 성능 등의 이슈에서 가장 많이 언급되는 공간이다.
힙 구성 방식이나 가비지 컬렉션 방법 등은 JVM 벤더의 재량이다.

3. 실행 엔진(Execution Engine)

3-1. 인터프리터

모든 코드는 초기에 인터피르터에 의해 시작되며, 한 줄씩 읽는 방식을 의미한다.

3-2. JIT 컴파일러, JIT Optimizer

💡
컴파일이란? 상위 레벨의 언어로 만들어진 것을 기계가 이해할 수 있는 코드로 변환하는 것을 말한다. 자바에서는 javac라는 컴파일러를 사용하여 컴파일한다. 컴파일 후에는 소스코드를 바이트 코드로 된 class라는 파일로 변환하게 된다.
컴파일 후 JVM이 동작하기 때문에 JVM에서는 항상 바이트 코드로 시작하고, 이를 동적으로 기계가 이해할 수 있는 코드로 변환한다.
인터프리터 효율을 높이기 위해, 인터프리터가 반복되는 코드를 발견하면 JIT 컴파일러로 반복되는 코드를 모두 네이티브 코드로 바꿔둔다. 이로 인해 같은 함수가 호출되면 매번 기계어 코드를 생성하는 것을 방지할 수 있다. 이후로는 캐싱된 코드를 사용하기 때문에 효율적으로 동작할 수 있다.

JIT 컴파일러는 어떻게 중복된 코드라고 인식할까?

메서드에는 아래 두 개의 카운터가 존재한다. 인터프리터에 의해 카운터가 증가되면 한계치에 도달했는지 확인하고, 도달했다면 인터프리터는 컴파일을 요청한다.
컴파일이 요청되면 컴파일 대상 목록들이 큐에 쌓인다. 별도의 컴파일러 스레드가 해당 큐를 모니터링한다.
수행 카운터(invocation counter)
메서드를 시작할 때마다 증가한다.
백에지 카운터(backedge counter)
높은 바이트 코드 인덱스에서 낮은 인덱스로 컨트롤 흐름이 변경될 때마다 증가한다.
메서드가 루프가 존재하는지 확인할 때 사용된다.
수행 카운터 보다 컴파일 우선순위가 높다.
한계치 공식 : ComplileThreshold * OnStackReplacePercentage / 100
CompileThreshold : 메서드가 n번 호출되었을 때, JIT에서 컴파일을 한다.
CompileThreshold : 35,000 & OnStackReplacePercentage : 80이라면 메서드가 3만 5천 번 호출되면 JIT에서 컴파일을 하고, 백에지 카운터가 35,000 * 80 / 100 = 28,000이 되었을 때 컴파일 된다.

HotSpot VM에서 JIT 외에 OSR(On Stack Replacement)이라는 특별한 컴파일도 수행된다.

오랫동안 루프가 지속되는 경우 사용된다.
컴파일이 완료된 상태에서 최적화되지 않은 코드가 수행되는 것을 발견한 경우, 인터프리터에 계속 머무리지 않고 컴파일된 코드로 변경한다.
루프가 끝나지 않고 지속적으로 수행되고 있을 경우 큰 도움이 된다.

3-3. GC(Garbage Collector)

더이상 참조되지 않는 객체를 모아서 정리한다.

4. JNI(Java Native Interface)

자바 애플리케이션에서 C, C++, 어셈블리로 작성된 함수를 사용할 수 있는 방법 제공
Native 키워드를 사용한 메소드 호출

5. 네이티브 메소드 라이브러리

C, C++로 작성 된 라이브러리

📌 JVM은 어떤 과정을 거칠까?

전체 과정

1.
JVM은 OS로부터 메모리를 할당 받는다. 할당 받은 메모리는 용도에 따라 영역을 나누어 관리한다.
2.
자바 컴파일러(javac)가 자바 소스코드(.java)를 읽어서 자바 바이트코드(.class)로 변환한다.
3.
Class Loader를 통해 자바 바이트코드(.class)를 JVM으로 로딩된다.
4.
로딩된 파일들은 Execution Engine을 통해 해석된다.
5.
해석된 바이트코드는 Runtime Data Areas에 배치되어 실질적인 수행이 이루어진다. 필요에 따라 GC가 이루어진다.
💡
[요약] 클래스 로더(Class Loader)가 컴파일된 자바 바이트코드를 런타임 데이터 영역(Runtime Data Areas)에 로드하고, 실행 엔진(Execution Engine)이 로딩된 파일들을 해석하고 실행한다.

📌 참고 자료

TOP