JVM(Java Virtual Machine) 이란?
JVM은 Java Virtual Machine의 줄임말로 직역하면 자바를 실행하기 위한 가상 컴퓨터라고 할 수 있다.
자바는 OS에 종속적이지 않는다는 특징을 가지고 있는데, OS에 종속받지 않고 CPU가 자바를 인식, 실행할 수 있게 해주는 것이 바로 JVM이다. 자바의 특징 중 하나인 자동 메모리 관리(Garbage Collection) 또한 JVM이 수행한다.
자바 소스코드, 즉 원시 코드(*.java)는 CPU가 인식 하지 못하므로 기계어로 컴파일 해줘야 한다.
하지만 자바는 JVM을 거쳐서 OS에 도달하기 때문에 OS가 인식할 수 있는 기계어로 바로 컴파일 되는게 아닌 JVM이 인식할 수 있는 바이트 코드(*.class)로 변환된다. 자바 코드를 컴파일 하여 바이트 코드(*.class)로 만들면 JVM이 OS가 바이트 코드를 이해할 수 있도록 해석해준다. 따라서 바이트 코드는 JVM 위에서 OS에 상관없이 실행될 수 있는 것이다.
JVM은 자바 실행 환경(JRE, Java Runtime Environment)에 포함되어 있다.
스택 기반 VM
JVM은 스택 기반의 가상 머신이다. 스택 기반 VM은 다음과 같은 장점을 가진다.
- 코드 작성과 컴파일이 쉽다.
- 속도는 레지스터 기반 VM 보다 느리지만 명령어의 크기가 작다.
- 대다수의 가상 머신이 스택 기반 VM을 채용한다.
- 하드웨어(레지스터, CPU 등)에 대해 직접 다루지 않아서 다양한 하드웨어에서 사용할 수 있다.
- 피연산자가 스택 포인터에 의해 암시적으로 처리된다.
자바 컴파일러(Java compiler)
자바 컴파일러(Java Compiler)는 JDK를 설치하면 bin에 존재하는 javac.exe를 말한다.
(즉, JDK에 Java Compiler가 포함되어 있음)
javac 명령어를 통해 .java를 .class로 컴파일 할 수 있다.
자바 바이트 코드(Java bytecode)란?
가상 컴퓨터(VM)에서 돌아가는 실행 프로그램을 위한 이진 표현법이다.
자바 바이트 코드(Java bytecode)는 JVM이 이해할 수 있는 언어로 변환된 자바 소스 코드를 의미한다. 자바 컴파일러에 의해 변환된 코드의 명령어 크기가 1바이트라서 자바 바이트 코드라고 불린다.
이러한 자바 바이트 코드의 확장자는 .class이다.
위의 그림처럼 서로 다른 운영체제라도 자바 가상 머신만 설치되어 있다면, 같은 자바 프로그램이 아무런 추가 조치 없이 동작할 수 있다. 따라서 개발자는 한 번만 프로그램을 작성하면, 모든 운영체제에서 같이 사용할 수 있는 장점이 있다.
단, 자바 프로그램과는 달리 JVM은 운영체제에 종속적이므로, 각 운영체제에 맞는 JVM을 설치해야 한다.
또한, 자바 프로그램은 일반 프로그램보다 JVM이라는 한 단계를 더 거쳐야 하므로, 상대적으로 실행 속도가 느리다는 단점이 있다.
JVM 구성
JVM(자바 가상 머신)은 다음과 같이 구성된다.
- 클래스 로더 (Class Loader)
- 실행 엔진 (Execution Engine)
- 인터프리터 (Interpreter)
- JIT 컴파일러 (Just-In-Time compiler)
- 가비지 컬렉터 (Garbage Collector)
- 런타임 데이터 영역 (Runtime Data Area)
- 메서드 영역
- 힙 영역
- PC Register
- 스택 영역
- 네이티브 메서드
- 네이티브 메서드 인터페이스 (Native Method Interface)
- 네이티브 메서드 라이브러리 (Native Method Library)
클래스 로더 (Class Loader)
JVM 내로 클래스 파일(*.class)을 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈이다. 자바는 동적으로 클래스를 읽어오므로, 프로그램이 실행 중인 런타임에서야 모든 코드가 JVM과 연결된다.
이렇게 동적으로 클래스를 로딩해주는 역할을 하는 것이 바로 클래스 로더(class loader)이다.
런타임 시 동적으로 클래스를 로드하고 로드된 바이트 코드(*.class)들을 엮어 JVM의 메모리 영역인 Runtime Data Area에 배치한다.
클래스 파일의 로딩 순서는 3단계로 구성된다. (Loading → Linking → Initialization)
- Loading (로드) : 클래스 파일을 가져와서 JVM의 메모리에 로드한다.
- Linking (링크) : 클래스 파일을 사용하기 위해 검증하는 과정이다.
- Verifying (검증) : 읽어들인 클래스가 JVM 명세에 명시된 대로 구성되어 있는지 검사한다.
- Preparing (준비) : 클래스가 필요로 하는 메모리를 할당한다.
- Resolving (분석) : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
- Initialization (초기화) : 클래스 변수들을 적절한 값으로 초기화한다. (static 필드들을 설정된 값으로 초기화 등)
실행 엔진 (Execution Engine)
클래스 로더에 의해 JVM으로 로드된 바이트 코드(.class)들은 Runtime Data Areas의 Method Area에 배치되는데, 배치된 이후에 JVM은 Method Area의 바이트 코드를 실행 엔진(Execution Engine)에 제공하여 바이트 코드를 명령어 단위로 읽어서 실행한다.
자바 바이트 코드(*.class)는 기계가 바로수행할 수 있는 언어보다는 가상 머신이 이해할 수 있는 중간 레벨로 컴파일된 코드이다. 그래서 실행 엔진은 이와 같은 바이트 코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경해준다.
이 수행 과정에서 실행 엔진은 인터프리터와 JIT 컴파일러 두 가지 방식을 혼합하여 바이트 코드를 실행한다.
인터프리터 (Interpreter)
자바 컴파일러에 의해 변환된 바이트 코드를 읽고 한줄씩 기계어로 해석하고 바로 실행한다.
JVM 안에서 바이트 코드는 기본적으로 인터프리터 방식으로 동작한다. 다만 같은 메서드라도 여러 번 호출이 됐을 때 매번 해석하고 수행해야 돼서 전체적인 속도는 느리다.
JIT 컴파일러 (Just-In-Time Compiler)
위의 인터프리터의 단점을 보완하기 위해 도입된 방식으로, JIT 컴파일러는 실행 시점에 인터프리터 방식으로 기계어 코드를 생성할 때 자주 사용되는 메서드의 경우 컴파일하고 기계어를 캐싱한다. 그리고 해당 메서드를 여러 번 호출할 때 매번 해석하는 것을 방지한다.
한줄씩 실행하는 것이 아니라 컴파일된 네이티브 코드를 실행하는 것이기 때문에 전체적인 실행 속도는 인터프리팅 방식보다 빠르다. 하지만 바이트 코드를 네이티브 코드로 변환하는 데에도 비용이 소요되므로, JVM은 모든 코드를 JIT 컴파일러 방식으로 실행하지 않고, 인터프리터 방식을 사용하다 일정 기준이 넘어가면 JIT 컴파일 방식으로 명령어를 실행하는 식으로 진행한다.
가비지 컬렉터 (Garbage Collector, GC)
자바 가상 머신은 가비지 컬렉터를 이용하여 Heap 메모리 영역에서 더는 사용하지 않는 메모리를 자동으로 회수해준다.
C언어 같은 경우 개발자가 직접 메모리를 해제해줘야 되지만, 자바는 이 가비지 컬렉터를 이용해 더는 사용하지 않는 메모리를 자동으로 회수하며 메모리를 관리해준다.
런타임 데이터 영역 (Runtime Data Area)
프로그램을 수행하기 위해 OS에서 할당받은 메모리 공간으로, 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역이다.
런타임 데이터 영역은 크게 Method Area, Heap Area, Stack Area, PC Register, Native Method Stack으로 나눌 수 있다.
모든 스레드가 공유해서 사용 (GC의 대상)
- 힙 영역 (Heap Area)
- 메서드 영역 (Method Area)
스레드마다 하나씩 생성
- 스택 영역 (Stack Area)
- PC 레지스터 (PC Register)
- 네이티브 메서드 스택 (Native Method Stack)
메서드 영역 (Method Area) (= Class Area = Static Area)
JVM에서 읽어들인 클래스와 인터페이스에 대한 런타임 상수 풀, 메서드와 필드, static 변수, 메서드 바이트 코드 등을 보관하는 영역이다.
런타임 상수 풀 (Runtime Constant Pool)
메서드 영역에 존재하는 별도의 관리영역으로, 상수 자료형을 저장하여 참조하고 중복을 막는 역할을 수행한다.
클래스와 인터페이스 상수, 메서드와 필드에 대한 모든 레퍼런스를 저장한다.
JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리 상 주소를 찾아 참조한다.
힙 영역 (Heap Area)
프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 메모리 영역이다. new 키워드로 생성된 객체와 배열을 저장한다. JVM이 관리.
스택 영역 (Stack Area)
프로그램 실행 과정에서 임시로 할당되었다가 메서드를 빠져나가면 바로 소멸되는 특성의 데이터(지역변수, 파라미터, 리턴 값, 연산에 사용되는 임시 값 등)를 저장하기 위한 영역이다.
메서드 호출 시마다 각각의 스택 프레임( 그 메서드만을 위한 공간)이 생성된다. 메서드 수행이 끝나면 프레임 별로 삭제를 한다.
PC 레지스터 (PC Register)
Thread가 시작될 때 생성되며 생성될 때마다 생성되는 공간으로, 스레드마다 하나씩 존재한다.
스레드가 어떤 부분을 어떤 명령으로 실행해야할 지에 대한 기록을 하는 부분으로 현재 수행 중인 JVM 명령의 주소를 갖는다. CPU 명령어 즉, Instruction을 수행한다. CPU instruction을 수행하는 동안 필요한 정보를 CPU 내 기억장치인 레지스터에 저장, 연산 및 결과값을 메모리에 전달하기 전 CPU 내 기억장치이다.
네이티브 메서드 스택 (Native Method Stack)
자바 프로그램이 컴파일되어 생성되는 바이트 코드가 아닌 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역이다. 자바가 아닌 다른 언어로 작성된 코드를 위한 공간. Java Native Interface를 통해 바이트 코드로 전환하여 저장하게 된다. 일반 프로그램처럼 커널이 스택을 잡아 독자적으로 프로그램을 실행시키는 영역.
JNI (Java Native Interface)
JNI는 자바가 다른 언어로 만들어진 애플리케이션과 상호 작용할 수 있는 인터페이스를 제공하는 프로그램이다.
JNI는 JVM이 Native Method를 적재하고 수행할 수 있도록 한다. 하지만 실질적으로 제대로 동작하는 언어는 C/C++ 정도 밖에 없다고 한다.
Native Method Library
C, C++로 작성된 라이브러리를 칭한다. 만일 헤더가 필요하면 JNI는 이 라이브러리를 로딩해 실행한다.
[Reference]
'Development > Java' 카테고리의 다른 글
가비지 컬렉션(GC, Garbage Collection) 알고리즘 (0) | 2024.12.17 |
---|---|
가비지 컬렉션 (GC, Garbage Collection) (0) | 2024.12.17 |
불변 객체(Immutable Object) (0) | 2024.11.21 |