Backend/Java

Java - JVM(Java Virtual Machine)에 대하여

퐁고 2023. 2. 7. 21:37
반응형

JVM(Java Virtual Machine)이란?

직역하면 '자바를 실행하기 위한 가상 기계(컴퓨터)'라고 할 수 있습니다.
Java 는 OS에 종속적이지 않다는 특징을 가지고 있습니다. OS에 종속받지 않고 실행되기 위해선 OS 위에서 Java 를 실행시킬 무언가가 필요한데 그게 바로 JVM입니다.

즉, JVM이란 OS에 종속받지 않고 CPU가 Java를 인식, 실행할 수 있게 하는 가상 컴퓨터입니다.

그림 해석

1. Java 소스 코드, 즉 원시 코드(*.java)는 CPU가 인식을 하지 못하므로 기계어로 컴파일을 해줘야 합니다.

2. 하지만 Java는 이 JVM 이라는 가상머신을 거쳐서 OS에 도달하기 때문에 OS가 인식할 수 있는 기계어로 바로 컴파일 되는게 아니라 JVM이 인식할 수 있는 Java 바이트 코드(*.class)로 변환됩니다.

3. 변환된 바이트 코드는 기계어가 아니기 때문에 OS에서 바로 실행되지 않습니다.

4. 이 때, JVM이 OS가 바이트 코드를 이해할 수 있도록 해석해줍니다. 따라서 바이트 코드는 JVM 위에서 OS 상관없이 실행될 수 있는 것입니다.

💡 여기서 
Java Compiler는 JDK를 설치하면 bin 에 존재하는 javac.exe를 말합니다. (즉, JDK에 Java Compiler가 포함되어 있다는 소리입니다.) javac 명령어를 통해 .java를 .class로 컴파일 할 수 있습니다.

JVM은 자바 실행 환경 JRE(Java Runtime Environment)에 포함되어 있습니다. 현재 사용하는 컴퓨터의 운영체제에 맞는 자바 실행환경 (JRE)가 설치되어 있다면 자바 가상 머신이 설치되어 있다는 뜻입니다.

 

JVM을 사용함으로써 얻는 가장 큰 이점

JVM을 사용하면 하나의 바이트 코드(.class)로 모든 플랫폼에서 동작하도록 할 수 있습니다.

즉, Java에서는 C언어와는 달리 JVM을 사용하기 때문에 각자의 플랫폼에 맞게끔 컴파일을 따로따로 해줘야 할 필요가 없습니다. 하나의 바이트 코드로 JVM이 설치되어 있는 모든 플랫폼에서 동작이 가능하다는 이야기입니다. 

 

※ Java는 플랫폼에 종속적이지 않지만 JVM은 플랫폼에 종속적이다.

이렇게 Java는 컴파일된 바이트코드로 어떤 JVM에서도 동작시킬 수 있기 때문에 플랫폼에 의존적이지 않습니다. 하지만 반대로 자바 가상 머신(JVM)은 플랫폼에 의존적입니다. 즉, 리눅스의 JVM과 윈도우의 JVM은 서로 다릅니다. 자바로 작성된 모든 프로그램은 자바 가상 머신에서만 실행될 수 있으므로, 자바 프로그램을 실행하기 위해서는 반드시 자바 가상 머신이 설치되어 있어야 합니다. 따라서 오라클은 대부분의 주요 운영체제뿐만 아니라 웹 브라우저, 스마트 폰, 가전기기 등에서도 자바 가상 머신을 손쉽게 설치할 수 있도록 지원하고 있습니다.

 

JVM은 바이트코드를 명령어 단위로 읽어서 해석하는데, Interpreter 방식과 JIT 컴파일 방식 두 가지 방식을 혼합하여 사용합니다. 먼저 Interpreter 방식은 바이트코드를 한 줄씩 해석, 실행하는 방식입니다. 초기 방식으로, 속도가 느리다는 단점이 있습니다.

인터프리터 (Interpreter) 란 ?

Interpreter(이하 인터프리터)는 컴파일러처럼 고레벨언어를 기계어(저레벨언어)로 해석해주는 번역 프로그램입니다. 컴파일러는 전체 소스코드를 보고 명령어를 수집하고 재구성하는 반면 인터프리터는 소스코드의 각 행을 연속적으로 분석하며 실행합니다. 때문에 일반적으로 각 행마다 실행하는 인터프리터보다는 컴파일러가 더 빠릅니다.

자바스크립트와 파이썬이 대표적인 인터프리터 언어고, C,C++은 컴파일언어, 자바는 컴파일러와 인터프리터 모두 해당됩니다.

JVM으로 다시 돌아와서 앞서 말씀드린 것처럼 인터프리터는 각 행마다 변환를 해주기 때문에 속도 측면에서 느립니다.

이를 개선하기 위하여 도입한 것이 바로 JIT compiler(Just-In-Time Compiler, 이하 JIT 컴파일러) 입니다. 

 

JIT (Just In Time) 컴파일러란?

기존의 자바는 인터프리터 방식으로 명령어를 하나씩 실행하게끔 이루어져 있어 실행 속도가 느렸습니다. 그렇다고, 바이트 코드 전체를 프로그램 수행 초기에 모두 컴파일을 하게 되면 초기 속도가 너무 느리게 됩니다. 하지만 하드웨어가 발전하면서 자바 컴파일러도 JIT 컴파일러 방식으로 개선되어 속도적인 측면에서 상당한 개선을 이루었습니다.

JVM은 JIT(Just In Time) 컴파일러라고 합니다. 또한, JIT 컴파일러는 같은 코드를 매번 해석하지 않고, 실행할 때 컴파일을 하면서 해당 코드를 캐싱해버립니다. 이후에는 바뀐 부분만 컴파일하고, 나머지는 캐싱된 코드를 사용합니다. 이렇게 JIT 컴파일러는 운영체제에 맞게 바이트 실행 코드로 한 번에 변환하여 실행하기 때문에 이전의 자바 해석기(Java interpreter) 방식보다 성능이 10배 ~ 20배 정도 더 좋습니다.

이렇게 컴파일된 코드를 네이티브 코드라고 하는데 네이티브 코드는 캐시에 보관되기 때문에 한번 컴파일된 코드는 빠르게 수행될 수 있습니다. JIT 컴파일러는 JVM의 핵심으로 JVM 내에서 성능에 가장 큰 영향을 줍니다.
 
간단히 정리하면, 인터프리터는 각 행마다 읽어 변환을 하고, JIT 컴파일러는 바이트 코드를 전체를 읽어 한꺼번에 변환을 합니다.

 

그럼 모든 코드를 JIT 컴파일러 방식으로 실행하면 되지 않을까?

바이트 코드를 Native Code로 변환하는 데에도 비용이 소요되므로, JVM은 모든 코드를 JIT 컴파일러 방식으로 실행하지 않고, 인터프리터 방식을 사용하다가 일정 기준이 넘어가면 JIT 컴파일 방식으로 명령어를 실행합니다.

 

Garbage Collector 또한 JVM에서 상당히 중요한 역할을 수행하는데 간단히 설명하자면

유효하지 않는 메모리, 즉 주소를 잃어버려서 사용할 수 없는 메모리를 Garbage 하는데 Garbage Collector는 메모리가 부족할 때 이런 Garbage 메모리에서 해제 시켜 다른용도로 사용할 수 있게 해주는 프로그램을 말합니다.

Garbage Collector를 수행할 땐 Garbage Collector를 수행하는 스레드를 제외한 모든 스레드들이 작업을 멈추고, 이후 완료되면 작업을 다시 시작합니다.

C, C++는 사용자가 메모리를 직접 해제해주어야 하지만 자바 같은 경우 Garbage Collector를 이 작업을 수행해줍니다.
주의할점은 메모리 누수까지 잡아주지는 않습니다.

*메모리 누수 : 프로그램 구동 중에 필요치 않은 메모리가 계속해서 점유하고 있는 현상