JVM이란?
Java Virtual Machine
JVM이란?
Java는 OS에 종속적이지 않다는 특징을 가지고 있다. OS에 종속받지 않고 실행되기 위해선
Java를 실행 시킬 무언가가 필요한데, 그게 바로 JVM이다.
즉, OS에 종속받지 않고 CPU가 Java를 인식, 실행할 수 있게 하는 가상의 컴퓨터이다.
JVM, 왜 필요할까?
Java 소스코드(*.java
)는 CPU가 인식을 하지 못하므로 기계어로 컴파일을 해줘야한다.
참고로 Java Compiler가 java파일을 class파일로 변환 시킨다.
Java는 JVM이라는 가상머신을 거쳐서 OS에 도달하기 때문에 OS가 인식할 수 있는 기계어로
바로 컴파일 되는 것이 아니라, JVM이 인식할 수 있는 Java bytecode(*.class
)로 변환된다.
변환된 bytecode는 기계어가 아니기 때문에 OS에서 바로 실행되지 않는다.
여기서 JVM이 필요한데 JVM은 bytecode로 이루어진 .class 파일을 OS가 이해할 수 있게
해석해주는 역할을 한다. 따라서 bytecode는 OS 종류에 관계없이 실행될 수 있는 것이다.
이렇게 JVM 덕분에 Java파일 만으로 어느 OS환경에서든 코드를 실행할 수 있는 것이다.
JVM의 구성요소
JVM 아키텍처에는 세 가지 주요 하위 시스템이 있다.
⒈ Class Loader (클래스 로더)
⒉ Runtime Data Area (런타임 데이터 영역)
⒊ Execution Engine (실행 영역)
하나 하나 자세히 정리해보자.
Class Loader
Java는 동적로드, 즉 컴파일 타임이 아니라 런타임(바이트 코드를 실행할 때) 때
클래스를 로드하고
링크하는 특징이 있다. 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더이다.
정리하자면, 클래스 로더는 런타임 중에 JVM의 메소드 영역에 동적으로 Java 클래스를 로드하는
역할을 한다. 그리고 클래스 로더에는 로딩
, 링크
, 초기화
단계로 나뉘어져 있다.
Loading
Loading 작업은 실행을 위해서 보조 메모리에서 주 메모리(RAM)으로 파일을 로드한다.
클래스로더는 .class 파일
을 읽고 해당 바이너리 테이터를 생성하여 메서드 영역에 저장한다.
JVM은 메서드 영역의 각 .class 파일
에 대한 일부 정보를 저장하는데, 이 정보는 다음과 같다.
- 로드된 클래스를 비롯한 그의 부모 클래스의 정보
- .class 파일이 인터페이스, 열거형 또는 클래스와 관련되어 있는지 여부.
- 관련 정보, 변수나 메소드등의 정보를 저장한다.
.class파일
을 로드한 후 JVM은 힙 메모리에서 이 파일을 나타내는 클래스 유형의 개체를 만든다.
개발자는 이 Class 객체를 사용하여 클래스 이름, 부모 이름, 메서드 및 변수 정보 등과 같은 클래스
수준 정보를 얻을 수 있다. 이 개체 참조를 얻으려면 Object 클래스의 getClass()
메서드를
사용할 수 있다.
[ 로더의 종류 ]
⒈ Bootstrap Class Loader
JVM 시작 시 가장 최초로 실행되는 클래스 로더이다.
부트스트랩 클래스 로더는 자바 클래스를 로드하는 것이 아닌, 자바 클래스를 로드할 수 있는
자바 자체의 클래스 로더와 최소한의 자바 클래스(java.lang.Object, Class, ClassLoader)만을
로드한다.
⒉ Extension Class Loader
확장 클래스 로더
는 부트스트랩 클래스 로더를 부모로 갖는 클래스 로더로서, 확장 자바 클래스들을
로드한다.
java.ext.dirs
환경 변수에 설정된 디렉토리의 클래스 파일을 로드하고, 이 값이 설정되어 있지
않은 경우 ${JAVA_HOME}/jre/lib/ext
에 있는 클래스 파일을 로드한다.
⒊ System Class Loader
자바 프로그램 실행 시 지정한 Classpath에 있는 클래스 파일 혹은 jar에 속한 클래스들을 로드한다.
쉽게 말하자면, 우리가 만든 .class
확장자 파일을 로드한다.
Linking
[ Verify(검증) ]
읽어 들인 클래스가 자바 언어 명세 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사.
[ Prepare(준비) ]
클래스가 필요로 하는 메모리를 할당하고, 메모리를 기본값으로 초기화한다.
[ Resolve(분석) ]
심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체한다.
여기서 잠깐! 심볼릭 레퍼런스란?🤔
심볼릭 레퍼런스는 참고하는 클래스의 특정 메모리 주소를 참조하는 것이 아니라,
참조하는 대상의 이름만 나타낸다.
이 후 class파일이 JVM에 올라가게 되면 심볼릭 레퍼런스는 그 이름에 맞는 객체의
주소를 찾아 연결하는 작업을 수행한다.
Initialization
• 클래스 변수들을 적절한 값으로 초기화 한다. 즉, static 필드들이 설정된 값으로 초기화한다.
Execution Engine
메모리 영역에 있는 데이터를 읽고 명령을 실행하는 역할을 한다.
Execution Engine은 아래의 세 가지 구성요소를 가지고 있다.
- Interpreter
- JIT Compiler
- Garbage Collector
Interpreter
바이트 코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나하나의 해석은 빠르지만 전체적인
실행속도는 느리다는 단점을 가진다. JVM안에서 바이트코드는 기본적으로 인터프리터 방식으로
동작한다.
JIT Compiler
JIT(Just-In-Time Compiler)는 인터프리터의 단점을 보완하기 위해 도입된 방식으로
바이트 코드 전체를 컴파일하여 네이티브 코드로 변경하고 이후에는 해당 메서드를 더 이상은
인터프리팅 하지 않고 네이티브 코드로 직접 실행하는 방식이다.
하나씩 인터프리팅하여 실행하는 것이 아니라, 바이트 코드 전체가 컴파일된 네이티브 코드를
실행하는 것이기 때문에 전체적인 실행 속도는 인터프리팅 방식보다는 빠르다.
네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 캐시에서 바로 꺼내어 실행하기
때문에 빠르게 수행된다. 하지만 JIT Compiler가 컴파일하는 과정은 바이트 코드를 하나씩 인터프리팅
하는 것보다 훨씬 오래 걸리기 때문에 JIT Compiler를 사용하는 JVM은 내부적으로 해당 메서드가
얼마나 자주 호출되고 실행되는지 체크하고, 일정 기준을 넘었을 때에만 JIT Compiler를 통해 컴파일
하여 네이티브 코드를 생성한다.
Garbage Collector
더 이상 참조되지 않는 객체를 모아서 정리한다.
참고
[ JNI (Java Native Interface) ]
자바 어플리케이션에서 C, C++, 어셈블리로 작성된 함수를 사용할 수 있는 방법을 제공한다.
[ 네이티브 메소드 라이브러리 ]
C, C++ 로 작성된 라이브러리
Runtime Data Area
JVM이 OS 위에서 실행되면서 할당받는 메모리 영역이 바로 런타임 데이터 영역
이다.
이 영역은 크게 5가지, 조금 세분화하면 6가지로 나뉜다.
PC Register
PC(Program Counter) Register는 현재 수행중인 명령의 주소를 가지며
스레드가 시작될 때 생성되며 각 스레드마다 하나씩 존재한다.
JVM Stack
프로그램 실행과정에서 임시로 할당되었다가 메소드를 빠져나가면 바로 소멸되는 특성의 데이터를
저장하기 위한 영역이다. 각종 형태의 변수나 임시 데이터, 스레드나 메소드의 정보를 저장한다.
메소드 호출 시마다 각각의 스택 프레임(그 메서드만을 위한 공간)
이 생성된다.
메서드 수행이 끝나면 프레임 별로 삭제를 한다. 메소드 안에서 사용되는 값들을 저장한다.
또 호출된 메소드의 매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장한다.
JVM 스택 역시 PC Register와 마찬가지로 스레드가 시작될 때 생성되며 각 스레드마다 하나씩
존재한다.
Native Method Stack
Java 외의 언어로 작성된 네이티브 코드를 위한 스택이다. JNI를 통해 호출하는 C, C++ 등의 코드를
수행하기 위한 스택으로, 언어에 맞게 스택이 생성된다.
예를 들어 C언어 이면 C스택이 생긴다.
Heap
인스턴스 또는 객체를 저장하는 공간으로 가비지 컬랙션
대상이다. JVM 성능 등의 이슈에서
가장 많이 언급되는 공간이다. Heap 구성 방식이나 가비지 컬렉션 방법 등은 JVM 벤더들의 재량이다.
Method Area (= Class Area = Static area)
모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. JVM이 읽어 들인 각각의 클래스와
인터페이스에 대한 런타임 상수 풀, 필드와 메서드에 대한 정보, static 변수, 메서드의 바이트 코드
등을 보관한다.
런타임 상수 풀(Runtime Constant Pool)
JVM 동작에서 가장 핵심적인 역할을 수행하는 곳으로 JVM 명세에서도 따로 중요하게 기술한다.
각 클래스와 인터페이스의 상수 뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는
테이블로 어떤 메서드나 필드를 참조할 때, JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의
실제 메모리상 주소를 찾아서 참조한다.
정리
JVM 동작 방식 총정리
⒈ 자바 컴파일러에 의해 자바 파일이 클래스파일로 컴파일 된다.
⒉ 클래스 로더가 클래스파일을 JVM에 로드한다.
⒊ 클래스 파일의 해당 바이너리 테이터를 생성하여 메서드 영역에 정보들(변수, 클래스 여부 등등)을 저장한다.
⒋ 로드 후 JVM은 힙 메모리에서 이 파일을 나타내는 클래스 유형의 개체를 만든다.
⒌ Execution Engine이 메모리에 존재하는 다양한 정보를 사용하여 클래스파일을 실행한다.