JVM (Java Virtual Machine, 자바 가상 머신)
- 자바 바이트 코드는
JRE
위에서 동작하는데, 이 JRE(JAVA API + JVM)
에서 바이트 코드를 해석하고 실행해주는 것이 바로 JVM
이다.
- JAVA 와 OS 사이에서 중개자 역할을 수행하며 JAVA가 OS에 구애받지 않고 재사용될 수 있도록 해주는 스택기반의 가상머신이다. 그래서 JAVA 바이트 코드를 실행하고자 하는 모든 HW에 JVM을 동작시키면 JAVA 실행 코드를 변경하지 않고 모든 종류의 HW에서 동작할 수 있게 된다.
- WORA(Write Once Run Anywhere) 를 구현하기 위한 가상머신
- JAVA 어플리케이션을 클래스 로더를 통해 읽어 들여
JAVA API
와 함께 실행된다.
JAVA API ?
JAVA 시스템을 제어하기 위해 JAVA 에서 제공하는 명령어들
바이트 코드
WORA를 구현하기 위해서 JVM 은 사용자 언어인 JAVA와 기계어 사이의 중간 언어인 자바 바이트코드
를 사용한다. 이 JAVA 바이트 코드
가 JAVA 코드를 배포하는 가장 작은 단위이다.
JVM 은 JAVA 바이트 코드
를 실행하는 실행기다. C/C++ 등의 컴파일러처럼 고수준 언어를 직접적으로 CPU 명령으로 변환하는게 아니라 JAVA 를 JAVA 바이트 코드
로 번역한다. 결과적으로 JAVA 바이트 코드
는 의존적인 플랫폼 코드가 없기 때문에 JVM 이 설치된 장비라면 CPU 나 OS 에 구애받지 않고 실행할 수 있는 것이다.
JAVA Code 수행 과정
- 자바 소스(확장자가 .java인 파일)를 자바 컴파일러(Java Compiler)가 자바 바이트 코드(JAVA Byte Code)로 컴파일한다.
- 컴파일 된 자바 바이트 코드를 클래스 로더가 동적로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩하고 링크해서 런타임 데이터 영역(JVM의 메모리 영역)에 로드한다.
- 실행 엔진이 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와 실행한다.
JVM 의 구조
1. 클래스 로더
JAVA 는 동적 로드, 즉 컴파일 타임이 아닌 런타임에 클래스를 처음 참조할 때 해당 클래스를 로드하고 링크하는 특징이 있다. 바로 이 동적 로드를 담당하는 부분이다. 클래스 로더는 다음과 같은 특징을 갖는다.
a. 계층 구조
b. 위임 모델
c. 가시성 제한
d. 언로드 불가
a. 계층 구조
- Bootstrap Class Loader
- 최상위 클래스 로더로 Native Code로 구현되어 있다.
- JVM 이 실행되는 시점에 메모리에 올라온다.
- Object class 비롯 JAVA API 들을 로드한다.
- Extension Class Loader
- 기본 JAVA API 를 제외한 확장 클래스들을 로드한다.
- 다양한 보안 확장 기능
- System Class Loader
- 부트스트랩 클래스로더와 익스텐션 클래스로더가 JVM 의 자체 구성요소들을 로드하는 것이라면, 시스템 클래스로더는 어플리케이션 클래스들을 로드한다고 할 수 있다.
- 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드한다.
- User-Defined Class Loader
- 어플리케이션 사용자가 직접 코드 상에 생성해서 사용하는 클래스들을 로드한다.
b. 위임 모델
바이트코드를 넘겨받은 클래스로더가 로드하다가, 혹은 실행 엔진에서 명령어 단위로 실행하다 처음으로 참조하는 클래스에 대해 클래스로더에게 로드를 요청할 때 클래스 로더는 다음 순서대로 요청받은 클래스가 있는지 확인한다.
1) 클래스로더 캐시
2) 상위 클래스로더
3) 자기 자신
계층 구조를 바탕으로 클래스 로더끼리 로드를 위임하는 구조로 동작한다. 계층 구조 상 먼저 상위 클래스 로더를 확인해 해당 클래스가 있다면 사용하고 없다면 로드를 요청받은 클래스로더가 클래스를 로드한다.
클래스 로더가 클래스 로드를 요청 받으면 이전에 로드된 클래스인지 확인 [클래스 로더 캐시] 하고, 없으면 상위 클래스 로더를 거슬러 올라가며 확인한다. 만약 부트스트랩 클래스 로더까지 올라갔는데도 해당 클래스가 없다면 요청받은 클래스 로더가 파일 시스템에서 해당 클래스를 찾게 된다.
- 위 그림은 클래스 로드 단계로, 클래스 로더가 아직 로드되지 않은 클래스를 찾으면 그림과 같은 과정을 거쳐 클래스를 로드하고 링크하고 초기화한다.
- Loading
- 클래스 파일에서 클래스를 가져와 JVM 메모리에 로드한다.
- Preparing
- 읽어 들인 클래스가 자바 언어 명세 및 JVM 명세에 명시된 대로 잘 구성되었는지 확인한다.
- 클래스 로드의 전 과정 중 가장 까다로운 검사를 수행하는 과정으로 시간이 가장 많이 걸린다.
- Resolving
- 클래스의 모든 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다
- 심볼릭 레퍼런스
- 참고하는 클래스의 특정 메모리 주소를 참조 관계로 구성한 것이 아닌 대상의 이름으로 참조하는 것
- 다이렉트 레퍼런스
- 참조하는 클래스의 특정 메모리 주소를 참조하는 것
- Initializing
- 클래스 변수들을 적절한 값으로 초기화한다. static initializer 들을 수행하고 static 필드들을 설정된 값으로 초기화한다.
c. 가시성 제한
하위 클래스 로더가 상위 클래스 로더를 확인할 때 상위 클래스 로더는 하위 클래스 로더에 있는 클래스를 확인할 수 없다.
d. 언로드 불가
로드하는 것은 가능하지만 언로드(Unload) 하는것은 불가능하다.
2. 런타임 데이터 영역(Runtime Data Area)
- PC Register
- PC (Program Counter) 레지스터는 각 스레드마다 하나씩 존재하고 JVM 명령의 주소를 갖는다.
- 스레드가 시작될 때 각 스레드 별로 생성되는 공간이다.
- JVM Stack
- 메소드의 매개변수, 지역 변수 등 메소드의 정보를 저장한다.
- 프로그램의 실행 과정에서 임시로 할당됐다가 메서드를 빠져나가면 소멸되는 데이터를 저장하기 위한 영역이다.
- JVM 스택은 각 스레드마다 하나씩 존재하고 스레드가 시작되는 시점에 생성된다.
- 스택 프레임(Stack Frame) 이라는 구조체를 저장하는 스택으로 JVM은 스택에 스택 프레임을 추가하고 제거하는 동작만 수행한다.
- 예외(Exception)발생 시 printStackTree() 등의 메서드로 보여주는 Stack Trace의 각 라인 하나가 스택 프레임을 표현한다.
- Natvice Method Stack
- 자바 외 언어로 작성된 네이티브 코드를 위한 스택이다.
- JNI(JAVA Native Interface)를 통해 호출되는 C/C++ 등의 코드를 수행하기 위한 스택으로 언어에 맞는 스택(C or C++) 으로 생성된다.
- Heap
- 인스턴스 또는 객체가 생성되면 저장하는 공간으로 GC(Garbage Collection)의 대상이 된다.
- JVM 성능 등의 이슈에서 가장 많이 언급되는 공간
- 객체를 저장하는 가상 메모리 공간으로 new 연산자로 생성된 객체와 배열을 저장한다
- 런타임 시 동적으로 할당하여 사용한다.
- Method Area
- 클래스 정보를 처음 메모리 공간에 올릴 때 초기화 되는 대상을 저장하기 위한 메모리 공간이다.
- 모든 쓰레드가 공유하는 영역으로, 클래스, 인터페이스, 메서드, 필드, static 변수 등의 바이트코드를 보관한다
- Runtime Constant Pool
- Method Area에 포함된 영역으로, JVM 에서 가장 핵심적인 역할을 수행하는 곳이다.
- 각 클래스와 인터페이스의 상수, 메서드와 필드에 대한 모든 레퍼런스를 담고 있는 테이블이다.
- 어떤 메서드나 필드를 참조할 때 JVM 은 런타임 상수 풀을 통해 해당 메서드나 실제 메모리 주소를 찾아 참조한다.
3. 실행 엔진
실행 엔진은 자바 바이트코드를 명령어 단위로 읽어서 실행한다. 실행 엔진은 바이트코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경하는데 그 방식은 다음 두가지가 있다.
a. 인터프리터
- 바이트코드 명령을 하나씩 읽고 해석하고 실행한다. 하나하나의 해석은 빠르지만 전체 실행 속도는 느리다는 단점이 있다.
- JVM 내 바이트코드는 기본적으로 인터프리터 방식으로 동작한다
b. JIT(Just-In-Time) 컴파일러
- 인터프리터의 단점을 보완하기 위해 도입된 방식
- 바이트코드 전체를 컴파일 해서 네이티브 코드로 변경하고 이후 해당 메서드를 더 이상 인터프리팅 하지 않고 네이티브 코드를 직접 실행하는 방식이다.
- 네이티브 코드는 캐시에 보관되기 때문에 캐시에 있는 컴파일된 코드를 꺼내서 실행하는건 빠르게 수행된다. 하지만 JIT 컴파일러가 컴파일하는 과정이 인터프리팅보다 훨씬 오래 걸리기 때문에 자주 사용해 일정 기준을 넘는 메서드만을 대상으로 네이티브 코드를 생상한다.
출처