JVM, Java Virtual Machine에 대한 정리 글
Java의 중요한 특징
Java의 첫번째 큰 특징은 플랫폼(OS) 독립적인 개발이 가능하다는 것이다. 플랫폼 독립적이라는 것은 구동되는 환경에 구애받지 않고 실행될 수 있도록 하고 다양한 환경 간 호환성을 유지한다는 뜻이다. 예를 들어 C 계열 언어로 작성된 프로그램들은 Windows 환경에서 빌드됐다면 그 프로그램을 그대로 macOS나 linux로 가져가서 실행할 수 없다. 그러나 Java로 작성된 프로그램은 플랫폼에 맞는 JVM만 설치되어 있다면 문제 없이 동작한다.
두 번째 큰 특징으로는, 자동으로 메모리를 관리해준다는 것이다. Java 이전의 엔지니어들이 프로그램 메모리를 스스로 관리했다면, Java 이후의 엔지니어들은 자동으로 메모리를 관리해주는 Garbage Collection을 사용하게 된다. 이 두 특징이 1995년 당시 혁신적인 개념으로 통했으며, 이때부터 이 두 개념을 가지고 있는 현대 프로그래밍 언어들이 등장하기 시작했다.
그런데 이 두 혁신적 개념을 가능하게 해준 숨은 공신이 있었으니, 그것이 바로 JVM(Java Virtual Machine)이다.
JVM
JVM은 Java Virtual Machine의 줄임말이다. 가상 머신이라는 것은 프로그램을 실행하기 위해 필요한 물리적 기계(하드웨어)를 소프트웨어로 구현한 것을 일컫는다. 그러므로 JVM은 Java 소스 파일(또는 Java Application)을 실행하기 위한 프로그램이라는 것을 유추할 수 있다. 그렇다면 Java를 실행하기 위한 프로그램이 JVM이라는 건데, JVM이 어떻게 Java 소스 파일을 실행하는지 알아보자.
먼저, javac(Java 컴파일러)가 java 소스 파일(.java)을 .class파일로 변환시켜 준다. 여기서 .class 파일은 JAVA Byte Code라고 하며 Byte Code는 기계어가 아니기 때문에(가상의 기계어) OS가 이해할 수 없다. 이 때 JVM이 OS가 Byte Code를 이해할 수 있도록 해석해주는 역할을 한다. 즉, 컴파일러는 Byte Code를 만들고, 이 코드가 OS에 따라 각기 다르게 해석되기 때문에 .java 파일은 OS에 맞는 JVM이 설치되어 있다면 어느 OS에서라도, 어느 기기에서라도 실행될 수 있게 되는 것이다. 위에서 언급했던 혁신적 개념 첫 번째가 이 과정을 통해 구현되는 것이다.
JVM이 클래스를 실행하는 과정
- 실행할 클래스 파일을 CLASSPATH에 등록된 디렉토리 경로에서 찾는다.
- 클래스 파일이 유효한 바이트코드(bytecode)인지 검사한다.
- 메모리에 바이트코드를 적재(load)한다.
- public static void main(String[] args) {} 블록을 찾는다.
- main() 블록에 들어 있는 코드를 실행한다.
JVM의 구조
그렇다면 JVM이 OS가 Byte Code를 이해할 수 있도록 해석하는 과정은 어떤 방식으로 진행되는지 JVM의 구조와 함께 살펴보도록 하자.
Java 소스 파일(.java)이 Java 컴파일러에 의해서 바이트코드로 변환된 뒤, JVM의 클래스 로더(Class Loader)에 의해 로드된다. 클래스 로더는 이렇게 런타임 중에 동적으로 저장된 클래스를 JVM 위에 탑재하고, 사용하지 않는 클래스를 메모리에서 삭제하는 역할을 한다.
그리고 이렇게 JVM에 로딩된 클래스의 바이트코드를 실행 엔진(Execution Engine)이 해석하여 바이너리 코드로 변환한다. 바이트코드는 비교적 인간이 보기 편한 형태로 기술되어 있기 때문에, 실행 엔진이 이를 기계가 수행할 수 있는 형태로 해석하는 역할을 한다.
실행 엔진의 해석 방법
실행 엔진이 바이트코드를 해석할 때 어떤 방식으로 해석하게 될까? Java가 개발된 뒤 초기에는 인터프리터 방식으로 바이트코드를 해석했다. 인터프리터 방식은 바이트코드를 명령어 단위로 한 줄씩 읽어서 수행하는 방식이었는데, 이 방식이 속도가 느린 인터프리터 언어의 단점을 그대로 계승하고 있었기 때문에 이 단점을 보완하기 위해 새로운 해석 방식이 도입되게 된다. 이것이 JIT(Just-In-Time) 방식이다.
JIT 방식은 바이트코드를 인터프리터 방식으로 해석하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드(Native Code)로 변경하고, 이후에는 더 이상 인터프리터 방식으로 해석하지 않고 네이티브 코드로 직접 실행하는 방식이다. 여기서 네이티브 코드라는 것은, CPU와 OS가 직접 실행할 수 있는 코드를 의미한다. 그리고 적절한 시점이라는 것은, 로딩된 바이트코드 전체에 JIT 방식을 적용하면 그만큼 큰 시간 비용이 발생하기 때문에 JVM이 내부적으로 특정 메소드가 얼마나 자주 실행되는지 체크해서, 일정 정도를 넘을 경우에만 적용한다는 것이다.
자바에서는 자바 컴파일러가 자바 프로그램 코드를 바이트 코드로 변환한 다음, 실제 바이트 코드를 실행하는 시점에서 자바 가상 머신(JVM, 정확히는 JRE)이 바이트 코드를 JIT 컴파일을 통해 기계어로 변환한다.
Runtime Data Area
런타임 데이터 공간(Runtime Data Area)는 JVM이 OS로부터 메모리를 할당받은 공간을 말한다.
쓰레드가 시작될 때, 각 쓰레드마다 PC 레지스터 영역, 스택 영역, Native Method Stack이 존재한다.
PC 레지스터 영역
각 쓰레드마다 하나씩 생성되는 영역. 현재 수행 중인 JVM 명령의 주소값을 저장한다. 여기서 JVM 명령이라는 것은, 쓰레드가 어떤 부분을 어떤 명령으로 실행해야 할 지 결정하는 부분을 말한다.
스택 영역
메소드 안에서만 사용되는 매개변수, 지역변수, 리턴값 등 지역적인 값이나 쓰레드나 메소드 그 자체에 대한 정보가 저장되는 영역이다. 메소드가 호출될 때 LIFO(Last In First Out) 방식으로 하나씩 생성되어 스택 영역이라고 부른다. 메소드 실행이 완료될 때마다 이 스택에서 해당 메소드의 값을 pop하여 하나씩 지워가는 방식으로 동작한다.
Native Method stack
다른 프로그램들처럼 커널이 독자적으로 Java 프로그램을 실행하는 공간이다. C,C++ 등 다른 언어의 메소드 호출을 위해 할당되는 구역 언어에 맞게 Stack이 형성되는 영역이다. 쉽게 말하면 기계어로 번역된 프로그램이 실행되는 곳이다.
이상이 스레드가 시작될 때마다 각 쓰레드마다 생성되는 영역이고, 이하는 하나의 Java 프로그램이 시작될 때 모든 쓰레드가 공유하는 영역이다.
메소드 영역
메소드 영역은 클래스 영역이나 스태틱 영역이라고도 부른다. 왜 그러냐면, 이 영역에 메소드 정보, 클래스 정보, static으로 선언된 변수 정보, 상수 정보가 담기기 때문이다. Java 코드를 작성할 때, 클래스 정보가 처음 메모리 공간에 올라갈 때 초기화하고자 하는 대상들에 대한 정보들이 담긴다. Java에서 static으로 선언된 변수는 항상 그 정보가 JVM 내에서 다른 변수들보다 우선하여 저장된다는 것을 알고 있을 것이다. Java 프로그램은 main 메소드로부터 호출하여 연속된 메소드 호출로 전체적인 프로그램의 흐름을 이어가기 때문에, 거의 대부분의 바이트코드가 이 영역에 저장된다. 변수와 메소드의 이름, (리턴) 데이터 타입, 접근 제어자, 형식 등 거의 대부분의 정보가 저장된다.
힙 영역
객체를 저장하는 가상 메모리 공간. Java 코드를 작성할 때 new 명령어를 사용해서 생성한 모든 인스턴스와 객체들이 이 영역에 저장된다. 그리고 이 글 초반에서 설명했던 Java의 아주 중요한 두 번째 특징인 "자동으로 메모리 관리"를 담당한 Garbage Collection Issue가 이 영역에서 일어난다.
결론
- JVM은 OS로부터 메모리를 할당받아 스스로 메모리 관리를 하고, 컴파일 후 생성된 파일을 바로 적재하므로 프로그램의 생성과 실행 모든 과정에 관여한다.
- JVM은 바이트코드를 구동하고 있는 플랫폼에 맞추어 바이너리 코드로 해석하기 때문에, 어느 플랫폼에서도 실행될 수 있으며 이는 플랫폼 독립적인 개발을 가능하게 해준다는 것을 의미한다.
- Java 컴파일러(javac)가 바이트코드를 만들 때 클래스 단위로 생성하기 때문에, 프로그램의 일부가 수정되더라도 전체를 컴파일할 필요가 없다.
+) 위 특징들로 인해 JVM 기반의 프로젝트에 여러 언어를 섞어 써도 오류가 발생하지 않는다.
+) JVM이 플랫폼 종속적이기 때문에 개발자로 하여금 플랫폼 독립적인 개발을 가능하게 해준다는 점이다. JVM은 OS에 따라 내부 해석 방식이 달라지므로 JVM 그 자체는 플랫폼 독립적이 아닌 플랫폼 종속적이다.
+ JDK와 JRE의 차이
JDK
Java Development Kit(자바 개발 키트)
Java를 사용하기 위해 필요한 모든 기능을 갖춘 Java용 SDK(Software Development Kit)이다.
JDK는 JRE를 포함하고 있다.
JRE에 있는 모든 것 뿐만 아니라 컴파일러(javac)와 jdb,javadoc과 같은 도구도 있다.
즉, JDk는 프로그램을 생성, 실행, 컴파일할 수 있다.
JRE
Java Runtime Environment(자바 런타임 환경)
JVM + 자바 클래스 라이브러리(Java Class Library)등으로 구성되어 있다.
컴파일 된 Java 프로그램을 실행하는 데 필요한 패키지이다.
JDK는 자바 프로그램을 실행, 컴파일 하는 개발용 도구
JRE, JVM을 모두 포함하는 포괄적인 키트이다.
JRE는 자바 프로그램을 실행할 수 있게 하는 도구이다. JVM을 포함하고 있다.
'Java' 카테고리의 다른 글
좋은 객체 지향 설계의 5가지 원칙(SOLID) (0) | 2023.02.15 |
---|---|
static 살짝 알아보기 (2) | 2023.02.04 |
리플렉션 : 스프링의 DI 동작 (0) | 2022.09.29 |
abstract class (0) | 2022.07.15 |
Features of Java (0) | 2022.07.04 |