Java를 실행시키는데 필요한 JVM에 대해 공부해보자 ❕
1. JDK와 JRE
Java 프로그램은 JVM위에서 실행되는데, JVM은 JDK나 JRE를 설치해 사용할 수 있다.
JDK (Java Development Kit)
Java를 사용하기 위해 필요한 모든 기능을 갖춘 Java용 SDK이다.
Java로 개발할 때 필요한 라이브러리들과 javac, javadoc 등의 개발 도구들을 포함하고, Java 프로그램을 실행시키기 위한 JRE도 포함되어 있다.
용도에 따라 다양한 배포판을 제공하는데, 대표적인 3가지 배포판은 다음과 같다.
- Java SE (Standard Edition)
- 표준 배포판. PC 응용프로그램과 서버 응용프로그램의 개발 환경이다.
- Java EE (Enterprise Edition)
- 기업용 배포판. 다중 사용자, 대규모 기업 응용프로그램 개발을 위한 환경이다.
- Java ME (Micro Edition)
- 모바일용 배포판. 제한된 리소스를 갖는 하드웨어에서의 응용프로그램 개발 환경이다.
JDK의 bin 디렉터리에 포함되어 있는 주요 개발 도구
- javac : 자바 컴파일러. 자바 소스 코드를 바이트 코드로 변환
- java : 자바 인터프리터. JVM을 실행시켜 바이트 코드를 해석하고 실행
- javadoc : 자바 소스로부터 HTML 형식의 API 도규먼트 생성
- jar : 자바 클래스 파일을 압축한 자바 아카이브 파일(.jar) 생성 및 관리
- jdb : 자바 응용프로그램의 실행 중 오류를 찾는 데 사용하는 디버거
- javap : 클래스 파일의 바이트 코드를 소스와 함께 보여주는 디어셈블러
JRE (Java Runtime Environment)
컴파일된 Java 프로그램을 실행하는데 필요한 소프트웨어들을 말하며, JVM과 자바 클래스 라이브러리 등으로 구성되어 있다.
개발자가 아닌 일반 사용자의 경우 자바 실행 환경만 필요하기 때문에 JRE만 따로 다운받아 사용하면 된다.
2. JVM (Java Virtual Machine)
JVM은 자바 가상 머신의 약자로, 자바 프로그램의 실행환경을 만들어 주는 소프트웨어이다. (JVM은 JRE에 포함되어 있다)
우리가 작성한 소스코드는 컴퓨터가 실행할 수 있는 기계어로 변환하는 컴파일과정이 필요하다. 자바 이전의 C/C++ 등 프로그래밍 언어들의 컴파일러는 소스코드를 기계어로 변환할 때 프로그램이 실행될 컴퓨터의 CPU와 운영체제에 종속적인 코드로 변환했다. 따라서 다른 *플랫폼(CPU나 운영체제가 다른 컴퓨터)에서 실행하려면 다시 컴파일하거나 플랫폼에 맞도록 코드를 수정해야 했다.
Java는 기존 프로그래밍 언어가 가진 플랫폼 종속성을 극복하여, 운영체제나 하드웨어에 상관없이 JVM이 있으면 어디서나 동일한 환경을 제공하여 같은 실행 결과를 기대할 수 있도록 한다. = WORA(Write Once Run Anywhere)
* 플랫폼 (platform) 간 호환성이 없는 이유 ?
- CPU마다 기계어가 다르다.
- 운영체제마다 API가 다르다.
- 운영체제마다 실행파일 형식이 다르다.
Java 소스 파일이 컴파일 되어 실행되는 과정은 다음과 같다.
- 컴파일러(javac)로 소스 파일(.java)을 컴파일해 *바이트 코드인 클래스 파일(.class)로 만든다.
- 이 (.class)파일은 JVM에 의해 기계어로 변환된다.
JVM을 사용하면 동일한 (.class) 파일로 모든 플랫폼에서 동일한 동작이 보장된다. (물론 플랫폼마다 설치해야하는 JVM은 다르다)
* 바이트 코드(byte code) ?
자바 소스코드가 JVM에서 이해할 수 있는 언어로 변환된 형태. 바이트 코드는 기계어가 아니라, 비교적 인간이 보기 편한 형태로 기술된 것이다. 어떤 CPU와도 관계없는 바이너리 코드(binary code)이다.
바이트 코드는 컴퓨터의 CPU에 의해 직접 실행되지 않고, JVM이 인터프리터 방식으로 실행시킨다.
JVM 특징
- 클래스 로더를 통해 바이트 코드를 읽어 들여 자바 API와 함께 실행시킨다.
- 컴파일된 바이트 코드를 컴퓨터가 이해할 수 있는 기계어로 변환시킨다.
- 자바와 OS 사이에서 중개자 역할을 수행하여, OS에 구애받지 않고 독립적으로 작동할 수 있도록 한다.
- 메모리 관리와 가비지 컬렉션(GC)을 수행한다.
⇒ JVM은 자바 프로그램이 어느 기기나 운영체제 상에서도 실행될 수 있도록 하고, 프로그램 메모리를 관리하고 최적화하는 것을 목적으로 한다.
3. JVM 실행 과정
JVM은 Class Loader, Runtime Data Area, Execution Engine으로 구성된다.
- 자바 프로그램이 실행되면 JVM은 OS로부터 메모리를 할당받는다.
(JVM은 할당받은 메모리를 용도에 따라 여러 영역으로 나누어 관리한다) - 자바 컴파일러(javac)가 자바 소스코드(.java)를 바이트 코드(.class)로 변환한다.
- Class Loader는 필요한 (.class)파일들을 로딩 및 링크하여 Runtime Data Area 영역(JVM 메모리 영역)에 올린다.
- Runtime Data Area에 로딩된 (.class)파일들은 Execution Engine을 통해 해석되고 실행된다. 이런 과정 속에서 JVM은 필요에 따라 Thread 동기화와 GC 같은 메모리 관리 작업을 수행한다.
4. JVM 구조
JVM을 구성하는 Class Loader, Execution Engine, Runtime Data Area를 살펴보자
4-1. Class Loader (클래스 로더)
- JVM 내로 클래스(.class)파일을 동적 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈이다.
- 실행 중에 사용할 클래스 파일들을 묶어서 JVM의 메모리 영역인 Runtime Data Area에 배치한다.
클래스 로드는 Loading → Linking → Initialization 과정을 통해 이루어진다.
- Loading : 클래스 파일을 가져와 JVM의 메모리에 로드
- Linking : 클래스 파일을 사용하기 위해 검증하는 과정
- Initialization : 클래스 변수들을 적절한 값으로 초기화
4-2. Execution Engine (실행 엔진)
- Runtime Data Area에 배치시킨 클래스(.class)파일들을 해석하고 실행한다.
- 바이트 코드를 기계어로 변환하는데, 이때 실행 엔진은 Interpreter, JIT 두 가지 방식을 이용한다.
- 가비지 컬렉션(GC)을 수행하는 모듈이 있다.
1) Interpreter (인터프리터)
인터프리터 방식은 바이트 코드를 명령어 단위로 읽어서 해석하고 실행한다.
JVM 내에서 바이트 코드는 기본적으로 인터프리터 방식으로 동작하는데, 한줄 씩 해석해 수행하고, 같은 메소드라도 호출될 때마다 매번 해석하고 수행하기 때문에 속도가 느리다.
2) JIT 컴파일러 (Just-In-Time Compiler)
인터프리터 방식의 단점을 보완하기 위해 도입된 방식이다.
인터프리터 방식으로 실행하다가 적절한 시점에 바이트 코드 전체를 컴파일하여 *Native Code(네이티브 코드)로 변경하고, 중복 호출되는 메서드는 더 이상 인터프리팅 하지 않고 캐싱해두었다가 네이티브 코드로 직접 실행한다.
네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 빠르게 수행된다. 하지만 바이트 코드를 네이티브 코드로 변환하는데 비용이 들고, 변환하는데 인터프리팅 방식보다 오래걸리므로 한 번만 실행되는 코드라면 인터프리팅 방식이 유리하다.
=> ^따라서 모든 코드를 JIT 컴파일러를 사용하지 않고 인터프리터 방식을 사용하다가 일정 기준이 넘어가면 JIT 컴파일 방식으로 실행해 실행 속도를 향상시키고, 코드를 최적화한다.^
* Native Code?
특정 플랫폼에서 직접 실행되는 기계어 코드를 의미한다. CPU 또는 운영체제에 종속적이며, 작성된 코드 그대로 컴퓨터에서 실행시킬 수 있다.
3) 가비지 컬렉터 (Garbage Collector, GC)
JVM은 가비지 컬렉터를 이용해 Heap 메모리 영역에서 더 이상 사용하지 않는 메모리를 자동으로 회수한다. 개발자가 따로 메모리를 관리하지 않아도 되므로 쉽게 프로그래밍 할 수 있는 장점이 있다.
일반적으로 GC는 자동으로 실행되고, 실행되는 시간이 정해져 있지 않다. (System.gc()
를 호출하면 가비지 컬렉션이 필요하다는 요청을 할 수 있다. 하지만 실제 실행은 보장되지 않는다. 가비지 컬렉션은 자바 플랫폼이 전적으로 판단하여 적절한 시점에 실행시킨다)
GC가 실행되면 GC역할을 수행하는 스레드를 제외한 나머지 모든 스레드들은 일시정지 상태가 된다.
4-3. Runtime Data Area (런타임 데이터 영역)
- JVM의 메모리 영역으로, 자바 프로그램을 실행하기 위해 OS에서 할당받은 메모리 공간이다.
- 런타임 시 클래스 데이터와 같은 메타 데이터와 실제 데이터가 저장되는 곳이다.
- 5가지 영역 Method, Heap, Stack, PC Register, Native Method Stack으로 나누어져 있다.
(이 중 Method 영역과 Heap 영역은 모든 스레드에서 공유한다)
1) Method Area (메서드 영역)
바이트 코드(.class)를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장한다.
모든 스레드가 공유하며, GC의 관리 대상이다.
JVM이 시작될 때 생성되고 JVM이 읽은 클래스와 인터페이스에 대한 메타데이터들을 프로그램이 종료될 때까지 저장한다.
모든 스레드가 공유하는 영역이라 다음과 같이 초기화 코드 정보들이 저장된다.
- Field Info : 멤버 변수의 이름, 데이터 타입, 접근 제어자의 정보
- Method Info : 메서드 이름, 리턴 타입, 함수 매개변수, 접근 제어자 정보
- Type Info : Class인지 Interface인지 저장, Type의 속성, 이름, Super Class의 이름
+ Runtime Constant Pool ?
Method 영역에 존재하는 별도의 관리영역으로, 각 클래스와 인터페이스의 상수 뿐만 아니라 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블로, 어떤 메서드나 필드를 참조할 때 JVM은 Constant Pool을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아 참조한다. 즉, 상수 자료형을 저장하여 참조하고 중복을 막는 역할을 한다.
2) Heap Area (힙 영역)
메서드 영역과 함께 모든 스레드가 공유하며, 동적으로 생성된 객체와 배열이 저장되는 영역이다.
`new` 연산자로 생성되는 클래스와 인스턴스 변수, 배열 타입 등의 Reference 타입이 저장되며, GC의 관리 대상이다.
Heap 영역은 효율적인 GC를 위해 다음과 같이 크게 3가지 영역으로 나누어져 있다.
- New/Young Generation : 자바 객체가 생성되자마자 저장되고, 생성된 지 얼마 안된 객체가 저장되는 공간
- Tenured Generation : Young Generation 영역이 차게 되면 참조정도에 따라 Tenured Generation 영역에 이동되거나 회수된다. (N/Y Generation에서 살아남은 객체들이 저장되는 공간)
- Permanent Generation : Class Loader에 의해 동적으로 로딩된 클래스의 메타데이터가 저장되는 공간 (^Java 7버전까지 Heap 영역에 존재, 8버전부터 Native Method Stack에 편입됨^)
힙 영역에 생성된 객체와 배열은 Reference 타입으로, JVM Stack 영역의 변수나 다른 객체의 필드에서 참조된다. 즉, 힙에 저장된 객체의 참조 주소는 Stack이 갖고 있고, 이를 통해서 힙 영역에 있는 인스턴스에 접근하는 것이다.
3) Stack Area (스택 영역)
프로그램 실행과정에서 임시로 할당되었다가 메서드를 실행하고 빠져나가면 바로 소멸되는 특성의 데이터를 저장하기 위한 영역으로, 각종 형태의 변수나 임시 데이터, 스레드, 메서드 정보 등을 저장한다.
메서드가 호출될 때마다 각 스택 프레임(그 메서드만을 위한 공간)이 만들어지며 메서드 수행이 끝나면 프레임 별로 삭제한다. (LIFO 구조)
스택 영역에서 기본 타입 변수는 직접 값을 가지고, 참조 타입 변수는 힙 영역이나 메서드 영역의 객체 주소를 가진다. 예를 들어, Member m = new Member()
와 같이 생성한 경우, Member 클래스는 힙 영역에 저장되고 생성된 클래스의 참조 변수 m과 해당 변수가 가리키는 객체의 주소는 스택 영역에 저장된다. (m이 가리키는 객체의 주소는 힙 영역에 있는 객체를 가리킨다)
4) PC Register (PC 레지스터)
현재 수행중인 JVM 명령의 주소를 저장하는 공간으로, 스레드가 어떤 부분을 무슨 명령으로 실행해야할 지에 대한 기록을 가진다.
스레드가 시작될 때 생성되며, 스레드마다 하나씩 존재한다.
일반적으로 프로그램 실행은 CPU에서 명령어를 수행하는 과정으로 이루어지는데, 이때 CPU는 연산을 수행하는 동안 필요한 정보를 레지스터에 저장해 사용한다. 자바는 OS나 CPU 입장에서 하나의 프로세스이기 때문에 CPU에 직접 연산을 수행하도록 하는 것이 아닌, 현재 작업하는 내용을 CPU에게 연산으로 제공하고 이를 위한 버퍼 공간으로 PC Register가 사용되는 것이다.
5) Native Method stack (네이티브 메서드 스택)
자바 소스 코드가 컴파일되어 생성된 바이트 코드가 아닌, 기계어로 작성된 프로그램을 실행시키는 영역으로 Java가 아닌 다른 언어로 작성된 코드를 실행시키기 위한 공간이다.
*JNI를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 스택이 생성되어 Native Method Stack 영역에 저장된다.
JIT 컴파일러에 의해 변환된 Native Code가 여기서 실행된다. 일반적으로 메소드를 실행하는 경우 JVM Stack 영역에 쌓이다가 해당 메서드 내부에 Native Code가 있다면 해당 코드는 Native Method Stack 영역에 쌓인다.
* JNI (Java Native Interface) ?
Java가 다른 언어로 만들어진 어플리케이션과 상호 작용할 수 있도록 하는 인터페이스를 제공하는 프로그램으로, 네이티브 코드를 바이트 코드로 변환해준다.
참고자료 😃
https://backendcode.tistory.com/161
https://asfirstalways.tistory.com/158
https://coding-factory.tistory.com/827
https://steady-snail.tistory.com/67