JAVA : JVM 이란

섬네일

 

Java의 JVM에 대해서 학습한 내용을 정리한 포스트입니다. 

🐻 JVM

JVMJava Virtual Machin의 약자입니다.

자바 관련 서적을 보게 되면 가장 먼저 자바의 실행 과정을 설명해 줍니다. Windows, Mac, LinuxOS에 종속적이지 않고 Java는 어느 환경에서도 실행할 수 있습니다. 이는 JavaWORA (Write Once, Run Anywhere) 정신을 가지고 만들어졌기 때문입니다. 

 

🐻 1. JVM 기본 배경

어떻게 Java가 어느 OS 환경에서든지 실행될 수 있는지 알아보겠습니다. 먼저 우리가 작성한 소스코드가 어떤 과정을 거치게 되는지 살펴보겠습니다.

출처 : JVM이란?

우리가 작성한 자바 코드는 컴파일러에 의해서 바이트 코드로 컴파일됩니다. 컴파일된 결과물이. class 파일입니다. 이를 JVM이 컴퓨터가 (OS 및 기반 하드웨어) 이해할 수 있는 네이티브 기계어로 해석하여 동작하게 됩니다.

C++ 같은 경우에는 특정한 운영체제, 하드웨어에서 실행되기 위해서 컴파일됩니다.

이 과정을 조금 더 자세하게 들여다보면, Java 소스 코드는 JDK에 내장된 Java 컴파일러를 사용하여 바이트코드라는 중간 상태로 컴파일됩니다. 이 바이트 코드는 명령어-피연산자 행이 포함된 16진수 형식이며, JVM은 이러한 명령어를 재컴파일 없이 OS 및 기반 하드웨어 플랫폼에서 이해할 수 있는 기계어로 해석할 수 있습니다.

정리하자면 바이트 코드를 JVM이 컴퓨터가 이해할 수 있는 기계어로 해석해 준다입니다. 바이트 코드 기반으로 JVM이 작동하기 때문에  OS와 하드웨어와 독립적일 수 있습니다. 

 

🐻 컴파일러, JVM

Java 소스 코드가 컴파일되고 작동하게 되는 방법을 가장 추상적으로 살펴보았습니다. 개발자는 Java를 이용하여 프로그램 개발, 컴파일, 디버깅 및 실행에 필요한 환경을 구성해야 합니다. 환경을 구성하기 위해서 JDK (Java Development Kit)라는 개발 키트를 다운로드합니다. 

JDK란 위에서 살펴본 과정에서 필요한 모든 것이 포함되어 있는 소프트웨어 개발 키트(SDK, Software Development Kit)입니다. JDK에 포함된 요소 중 JVM이 존재하고, compiler가 존재합니다.

출처 : geeksforgeeks

JVM을 알기 위해서 하나하나 관련 용어와 개념들을 정리해 보겠습니다.

 

🐻 2. JDK

자바 소스 코드를 컴파일하고, JVMOS가 해석할 수 있는 기계어로 번역하는 과정을 살펴보겠습니다.

출처: Medium

프로그래머가 작성한 소스 파일은 JDKCompiler에 의해서 바이트 코드로 변환됩니다. 변환된. class 파일은 JRE (Java Runtime Environment)를 통해서 실행되게 됩니다. 

정리하면 작성된 소스 코드가 JDK의 컴파일러에 의해 바이트 코드로 변환되고 JRE에 의해 실행됩니다. 그리고 JRE의 JVM에 의해서 OS가 이해할 수 있는 기계어(Binary Code)로 바로 변환되어 CPU에서 실행됩니다.

 

🐻 JRE

Java 코드가 실행되기 위해서 JRE가 존재한다는 것을 알게 되었습니다. 

JREJava Runtime Environment의 약자입니다. JVM과 자바 프로그램을 실행(동작)시킬 때 필요한 라이브러리 API를 묶어서 배포되는 패키지입니다. 이중 JVM의 아키텍처를 살펴보겠습니다.

출처 : scaler

JVM의 컴포넌트는 크게 4가지입니다.

  • 클래스 로더 서브 시스템 (Class Loader Subsystem)
  • 런타임 데이터 영역 (Runtime Data Area)
  • 실행 엔진 (Execution Engine)
  • 자바 네이티브 인터페이스 (Native Method Interface, JNI)

크게 네 가지의 컴포넌트가 존재합니다. 컴포넌트는 또 여러 가지 내부적으로 알아야 하는 용어와 개념들이 존재합니다. 이에 컴포넌트 하나하나 살펴보겠습니다.

 

🐻 3. JVM중간 간단 정리

JVM의 아키텍처와 개념들에 대해서 간단하게 살펴보았습니다. 구성요소를 살펴보기 전에 지금까지 나온 내용을 간단하게 정리해 보겠습니다.

JVM은 자바 가상 머신입니다. JVM은 바이트 코드로 컴파일된 소스를 기계어로 번역하고 실행할 수 있도록 만들어주는 소프트웨어입니다. 키워드별로 정리하면 다음과 같습니다.

  • 자바로 개발된 프로그램, 소스 코드를 컴파일러가 바이트 코드로 컴파일합니다.
  • 컴파일된 바이트 코드를 JVMOS가 실행 가능하도록 기계어로 번역합니다.
  • OS 제약 없이 동작할 수 있다.

 

🐻 4. Class Loader Subsystem

클래스 로더 서브시스템은 RAM에 클래스 파일을 가져옵니다. 클래스 로더는 JVM안에 존재하며 RAM에 로드해서 실행할 수 있도록 도와줍니다. JVM이 메모리에 클래스를 적재할 수 있는 이유는 OS로부터 메모리 할당을 받아 로딩(Loading), 링크(Linking), 초기화(Intiailzation) 과정을 거치기 때문입니다. 클래스 로더는 결과를 엮어서 메모리(Runtime Data Area)로 적재합니다.

Loading은 클래스 파일을 탑해자는 과정, Linking 은 클래스 파일을 사용하기 위해 검증하고, 기본 값으로 초기화하는 과정입니다. Initialization static fiend의 값들을 정의한 값으로 초기화하는 과정입니다. 

좀 더 자세히 알고 싶으신 분은 테코블 포스터를 참조해 주세요 😀

 

🐻 Loading

Loading에서는 Class Loader가 필요한 클래스 파일을 찾아서 메모리에 적재하는 일을 합니다. 클래스 파일이 기본으로 제공받는 클래스 파일인지 혹은 개발자가 정의한 클래스 파일인지와 같은 기준에 의해서 ClassLoader는 세 가지로 나뉘게 됩니다.

Application Class Loader

애플리케이션 / 시스템 클래스 로더라고 합니다. Classpath에 있는 클래스들을 탑재합니다. 개발자가 자바 코드로 짠 클래스 파일들을 JVM에 탑재하는 역할을 합니다. 

Extension Class Loader

확장 클래스 로더라고 합니다. 부모 로더인 부트스트랩 로더에 클래스 로딩을 위임하고, 실패할 경우 $JAVA_HOME/jre/lib/ext 황장 경로에 있는  Java Class의 라이브러리들을 JVM에 탑재하는 역할을 합니다.

Bootstrap ClassLoader

부트스트랩 클래스 로더는 $JAVA_HOME/jre/lib 디렉터리에 있는 JVM을 구동시키기 위한 가장 필수적인 라이브러리의 클래스들을 JVM에 탑재합니다. C/C++와 같은 네이티브 언어로 구현되어 있으며, 모든 클래스 로더의 부모 역할을 합니다.

Native CodeUnmanaged code라고도 불립니다. 메모리에 할당된 것을 프로그래머가 직접 해제해줘야 하는 기계어입니다. JVM은 C, C++로 만들어졌습니다.

 

🐻 Linking 

출처 : Medium

링킹은 클래스 로딩 단계에서 로드된 바이트 코드를 JVM에서 사용할 수 있도록 Java 런타임에 통합하는 프로세스입니다. 클래스 파일들을 검증하고, 사용할 수 있게 준비하는 과정을 의미합니다. Linking 또한 Verification, Preparation, Resolution이라는 세 단계로 이루어져 있습니다.

Verification

.class 파일의 정확성을 검증합니다. 컴파일러가 변환한 바이트 코드가 적절한지 여부를 검증합니다. 가장 복잡한 테스트 과정이며, 시간이 가장 많이 소요됩니다. 검증에 실패하면 런타임 오류 (java.lang.VerifyError)가 발생합니다.

관련 에러는 다음 참조 글에서 더 자세히 알 수 있습니다.

Preparation

클래스 및 인터페이스에 필요한 static field 메모리를 할당하고, 이를 기본값으로 초기화를 합니다. 기본값으로 초기화된 static field 값들은 Initialization 과정에서 코드에 작성한 초기값으로 변경이 됩니다. 기본값으로 초기화시키기 때문에 클래스파일의 코드를 작동시키지는 않습니다.

Resolution

Symbolic referencedirect reference로 대체됩니다. 이는 참조하고자 하는 대상의 이름만 가지고 참조 관계를 구성하는 것이 아닌, 실제 객체의 메모리 주소를 참조하게 됩니다. 메모리에 할당된 실제 주소를 코드에 반영하고, 실행 가능한 코드가 되는 것입니다.

 

🐻 Initialize

Class Loader sub system의 마지막 단계로, 모든 정적 변수에 정의된 원래 값이 할당되고 정적 블록이 실행됩니다. 클래스나 인터페이스의 초기화 로직도 실행됩니다. 이제 JVM에서 클래스 파일을 구동시킬 준비가 끝나게 됩니다.

 

🐻 5. Runtime Data Area

출처 : devkuma

Runtime Data AreaJava 프로그램이 OS에서 실행될 때 할당되는 메모리 영역입니다. Class Loader 가 JVM의 Runtime Data Area에 저장을 하게 되는 것입니다. 그리고 메모리를 효율적으로 관리하기 위해서 JVM은 용도에 따라 그림과 같이 여러 영역으로 구분하여 나눠서 관리하게 됩니다.

Runtime Data Area는 크게 Method Area, Heap Area, Stack Area, PC Register, Nativ Method Stack으로 나뉘게 됩니다. 이에 관련해서 간단하게 하나씩 살펴보겠습니다. 이후에 추가로 더 자세히 정리하여 포스팅하겠습니다.

 

🐻 Method Area

메서드 영역은 클래스에 대한 정보 (객체 구조, 생성자, 필드)와 변수(static variable)가 저장되는 영역입니다. 이 영역에 저장된 내용은 프로그램이 시작 전에 로드되고 프로그램이 종료 시 소멸됩니다. 모든 스레드에서 공유하는 리소스로 클래스 정보를 제공해 주는 역할을 합니다

 

🐻 Heap Area

힙 영역은 모든 인스턴스 변수 및 배열에 대한 정보를 저장합니다. JVM1개만 존재하는 공유자원입니다. Method Area는 메모리에 할당되는 인스턴스의 정보를 가지고 있다면, Heap Area는 실제로 할당된 데이터가 존재하는 공간입니다. 그래서 Method Area를 Heap Area의 Logical part이라고 표현합니다.

이곳에서는 문자열에 대한 정보를 가진 String Pool, 실제 데이터를 가진 인스턴스, 배열등이 저장됩니다. 이러한 데이터는 모든 Java Stack 영역에서 참조되어, Thread 간 공유됩니다. 그렇기에 데이터들은 thread safe 하지 않게 되었고, 동기화 문제를 해결해야 합니다. 동기화와 관련 글은 여기를 참조해 주세요. (스레드 시리즈)

 

🐻 Stack Area

스택 영역은 자바 프로그램에서 메서드가 호출될 때, 메서드 호출을 저장하기 위해 별도로 생성되는 스택을 저장하는 영역입니다. 작성한 프로그램을 실행시키기 위해서 main( )  메서드를 이용해서 프로그램을 시작합니다. 이때 main 스택 프레임이 생성됩니다. 그리고 내부적으로 호출되는 각각의 메서드마다 스택 프레임이 생성되고 삭제됩니다.

각 스택 프레임은 메서드에 대한 정보를 가지고 있는 Local Variable, Operand Stack, Constant Pool Reference로 구성되어 있습니다. 이와 관련한 자료는 이 글을 참조해 주세요

Stack Area는 각 Thread 별로 따로 할당되는 영역입니다. 공유 리소스가 아니므로 힙 영역과 다르게 스레드로부터 안전하다는 특징이 있습니다. 

 

🐻 PC Regitser

Program Counter Register는 스레드당 1개씩 존재하며 현재 실행하는 명령어의 주소와 끝나면 다음 실행될 명령어의 주소를 가리키는 역할을 합니다. 

 

🐻 Native Method Stack

네이티브 메서드 스택은 JNI(Java Native Interface)를 통해 호출되는 메서드 정보를 저장하기 위한 스택입니다. JNIJava 언어 외 C, C++ 언어로 작성된 네이티브 코드를 호출할 수 있는 인터페이스입니다. 다른 프로그래밍 언어로 작성된 메서드를 다루는 스택 영역으로, C Stacks라고도 불립니다.

Java로만 프로그래밍 언어를 구성하지 않고 네이티브 언어 (C, C++) 라이브러리와 연결하는 것은 레거시 데이터 또는 성능 상의 이유로 사용되었습니다. 자바의 라이브러리도 발전을 하면서 점차 쓰이지 않게 되었다고 합니다. 일반적으로 네이티브 코드는 다음과 같다고 합니다.

public native void method();

 

🐻 Runtime Data Area 정리

Rumtime Data Area에 대한 많은 정보를 보았지만 더 많은 개념들이 존재합니다. JVM은 프로그램을 실행하기 위해서 지금까지 살펴본 런타임 데이터 영역에 OS로부터 할당된 메모리 영역에 실행에 필요한 데이터를 올려 작동하게 됩니다. 5가지의 구성 요소들에 대해서 살펴보았고, 각 영역이 무슨 역할을 하는지 알아보았습니다. 특히 JVM의 메모리 영역에 대한 개념들은 중요하여 GC, Heap과 관련된 개념도 알아야 합니다. 이와 관련해서는 다음 글에서 정리해 보겠습니다.

 

🐻 6. Execution Engine

출처 : 실행 엔진(Execution Engine)

Execution Engine은 메모리(Runtime Data Area)에 적재된 클래스(바이트 코드)들을 네이티브(기계어)로 변경하여 명령어 단위로 실행합니다. 이 수행 과정에서 인터프리터와 JIT 컴파일러 두 가지 방식을 혼합하여 바이트 코드를 실행합니다. 

구성요소로는 Interpreter, JIT Compiler, Garbage Collector 가 존재합니다. 이에 관해서 하나씩 살펴보겠습니다.

Execution Engine은 바이트 코드를 운영체제에 맞게 해석해 주는 역할을 수행합니다.
@warning
클래스 로더에 의해 JVM으로 로드된. class 파일(바이트 코드) 들은 Runtime Data Ares의 Method Area에 배치되는데, 배치된 이후에 JVMMethod Area의 바이트 코드를 실행 엔진에 제공하여 정의된 내용을 바이트 코드로 실행시킵니다. 이때, 로드된 바이트 코드를 실행하는 런타임 모듈이 Execution Engine입니다.

 

🐻 Interpreter

Interpreter는 바이트 코드를 해석하고 명령어를 하나씩 실행합니다. Java Compiler에 의해서 프로그래머가 만든 소스 파일이 바이트 코드. class 파일이 만들어졌습니다. 이후 Class Loader를 통해서 Runtime Data Area에 적재되었습니다. Interpreter는 적재된 바이트 코드를 실행시키는 역하을 합니다.

Java는 위 과정을 통해서 운영체제에 종속되지 않을 수 있었습니다. 특히 WORA(Write Once Run Anywhere)를 실현하고 이식성이 높은 언어가 될 수 있었던 건 인터프리터 덕분입니다. 

상대적으로 속도의 문제가 발생할 수 있습니다. 변환된 바이트 코드 역시 실행되기 위해서는 기계어로 변환되어야 하기 때문에 컴파일 시점에 기계어로 변경되는 C, C++ 언어에 비해 속도가 느립니다. 매번 인터프리터가 코드 각 줄을 읽고 번역을 해야 하기 때문입니다. 

 

🐻 JIT Compiler

JIT CompilerJust In Time Compiler의 줄임말입니다. JITInterpreter가 바이트 코드를 기계어로 변환해 실행해 상대적으로 느린 단점을 해결하기 위한 역할을 수행합니다. 즉, 자바는 바이트 코드로 컴파일된 코드를 인터프리터가 머신이 수행할 수 있도록 네이티브 코드로 변환을 하는데, 반복되는 메서드 호출을 JIT Compiler가 캐싱을 해두고 네이티브 코드를 직접 제공하여 효율성을 향상합니다.

하지만 JIT Compiler의 경우에도 Interpreter가 해석하는 것보다 컴파일하는데 더 많은 시간이 소요합니다. 한 번만 실행되는 코드 세그먼트의 경우 Interpreter로 해석하는 것이 효율적입니다. 왜냐하면 JIT Compiler는 네이티브 코드를 캐시에 저장되는데, 매우 값비싼 리소스이기 대문입니다. 최대 효율을 위해서 JIT Compiler는 내부적으로 각 메서드 호출 빈도를 확인하고, 내부 알고르짐에 의해서 네이티브 코드를 컴파일하도록 결정합니다.

출처 : JIT Compiler란?

 

🐻 Garbage Collector

Garbage Collecto 는 줄여서 GC 라고도 불립니다. Heap Area에서 참조되지 않은 객체를 제거하는 역할을 합니다. Java에서는 자동으로 작동하지만, System.gc() 를 통해서 직접 호출할 수도 있습니다. 하지만 실행이 보장되는 것은 아닙니다.

Garbage Collector와 관련된 자세한 내용은 다음 글을 읽어주세요.

 

🐻 7. JNI, Native Method Libraies

출처: Native Method

JNIJava Native Method Interface 의 약어입니다. JNI는 인터페이스로 네이티브 메서드 라이브러리와 상호작용을 통해서 Java에서 C, C++ 라이브러리를 호출하는 역할을 합니다. Native Method Libraies는 C/C++로 작성되어 있습니다. 즉 Java 프로그램이 다른 언어로 작성된 프로그램과 상호작용을 돕는 역할을 합니다.

Java 에서 JNI를 사용하는 주된 이유는 Java 만으로는 할 수 없거나 비효율적인 작업을 처리하기 위해서입니다. 또는 기존 네이티브 라이브러리를 재사용하고 하드웨어를 직접 제어 또는 드라이브 연동 등을 위해서입니다.

 

🐻 8. 정리

지금까지 Java 가 어떻게 소스코드를 컴파일하고 프로그램이 실행되는지 살펴보았습니다. 그 과정에서 JVM에 대해서 알게되었습니다. 내부적으로 많은 개념들이 존재했습니다. 관련해서 간단하게 살펴보았는데, JVMRuntime Data Area에 존재하는 메모리 관련, GC등 알아햐 하는 기본 개념들이 존재합니다. 

다음 포스트는 JVM의 메모리와 GC에 관련해서 학습 정리한 후 포스트하도록 하겠습니다.  😀

 

😀 9. 출처