자바는 운영체제에 독립적으로 실행이 가능한 고수준의 객체지향 프로그래밍 언어입니다. 자바의 아버지인 제임스 고슬링에 따르면 다음과 같은 특징을 지닙니다.
- 간단하다 (Simple)
- 객체 지향적이다 (Object Oriented)
- 분산처리를 지원한다 (Distributed)
- 멀티쓰레드를 지원한다 (Multithreaded)
- 동적 로딩을 지원한다 (Dynamic)
- 실행환경에 대해 중립적이다 (Architecture neutral)
- 이식성이 높다 (Portable)
- 고성능 (High Performance)
- 신뢰성이 높다 (Robust)
- 보안성을 갖는다 (Secure)
이 특징들 중 많은 부분이 JVM(Java Virtual Machine, 가바 가상 머신)으로 인해 가능합니다. 이 글에서는 JVM의 구조와 역할에 대해서 정리해보고자 합니다.
참고로 JVM은 .class 형식의 바이너리 코드를 어느 운영체제에서나 실행시키기 위한 가상 머신입니다. JVM의 구현 상세는 Java를 소유한 Oracle 홈페이지에서 볼 수 있으며, 해당 상세를 충실히 구현하고 Java와의 호환성에 대한 인증을 통과하면 누구나 JVM을 만들 수 있습니다. 벤더사들마다 구현한 JVM의 구조가 다르기에 Oracle에서 만든 JVM인 Hotspot을 기준으로 내용을 정리했습니다.
참고 : Hotspot이란 이름은 내부적인 우선순위에 따라 캐싱되어야 하는 부분, 즉 ‘핫스팟’을 찾아 JIT 컴파일러로 컴파일하기 때문에 지어졌습니다. 이에 따라 서로 다른 목적으로 설계된 Hotspot Client JVM과 Hotspot Server JVM은 다른 JIT 컴파일러를 가지고 있습니다.
JVM 아키텍처
Class Loader
바이트코드 상태인 .class 파일을 JVM 안으로 로딩하고 Runtime Data Areas로 배치합니다. 자바의 동적 로딩이란 클래스로더가 Class를 사용되는 시점에 로딩하기 때문에 이루어집니다. 클래스로더에서는 3가지 일을 수행합니다.
- 로딩 (loading) : .class 파일을 읽어들여 바이너리 코드로 변환하고 Method Area에 저장합니다. 또한 Heap 영역에 해당 타입의 Class 객체를 생성합니다.
- 링킹 (linking) : 클래스에 필요한 메모리를 할당하고 연관된 개체의 주소를 매핑합니다. 총 세 단계로 이루어져있습니다.
- 초기화 (initialization) : 모든 static 변수를 코드에 정의된 값으로 할당해줍니다. 이 과정은 클래스 내부에서는 위에서 아래로, 클래스 계층 구조에서는 부모에서 자식 순서로 수행됩니다.
Runtime Data Areas
실행에 필요한 정보들을 저장하는 영역입니다. Method Area와 Heap은 공유 자원으로, PC Register와 Stack은 각 쓰레드마다 생성됩니다.
- Method Area : 클래스 이름, 부모 클래스, 메서드와 변수 정보, static 변수 등 모든 클래스 수준 정보가 저장되어 있습니다. 코드로는 Object.getClass()를 통해 얻을 수 있는 정보라고 생각하면 됩니다.
- Heap : 객체를 저장하는 영역입니다. 예를 들어 new 를 통해 인스턴스를 동적으로 생성하면 Heap 영역에 저장됩니다. 또한 Garbage Collector의 대상이 되는 영역입니다.
- Program Counter Registers : Execute Engine에서 다음번에 실행되어야 하는 코드의 주소를 저장되는 영역입니다.
- JVM Stack : 메서드 호출 정보 및 지역 변수 등이 저장되는 영역입니다. 메서드가 호출될 때마다 스택 프레임이 생성되며 해당 메서드의 모든 로컬 변수가 해당 프레임에 저장됩니다. 메서드가 종료된다면 해당 스택 프레임은 제거됩니다.
- Native Method Stack : Java로 작성되지 않은 메서드의 정보가 저장되는 영역입니다.
Execution Engine
바이트코드를 한 줄씩 읽어들여 실행합니다. 세 부분으로 분류할 수 있습니다.
- 인터프리터 (InterPreter) : 바이트코드를 한 줄씩 운영체제가 해석할 수 있는 기계어로 바꿔 실행합니다. 덕분에 Java가 실행환경에 중립적이라는 특징을 가집니다. (하지만 JVM은 실행환경에 종속적입니다) 인터프리터 언어 특징상 컴파일 언어에 비해 느리다는 단점이 있는데 JVM에서는 이를 보완하기 위해 JIT 컴파일러를 사용합니다.
- JIT 컴파일러 (Just-In-Time Compiler) : 기계어로 번역한 바이트코드를 저장한 뒤 인터프리터에서 반복되는 부분을 호출할 때마다 JIT 컴파일러가 해당 부분에 대한 기계어를 제공합니다. 캐시의 원리를 이용한다고 이해하면 됩니다. 이와 관련해 JVM Warm-up이라는 개념이 등장합니다. 프로그램이 처음 실행될 때 JIT 컴파일러에 저장된 기계어가 없기에 실행속도가 느립니다. 그래서 필요하다면 강제로 로직을 호출하여 기계어를 캐싱하는 작업을 할 수 있고 이를 Warm-up이라고 합니다.
- 가비지 컬렉터 (Garbage Collector, GC) : 힙 영역에서 특정한 알고리즘을 통해 참조되지 않은 객체를 제거합니다. Java에서는 GC 덕분에 개발자가 따로 메모리 할당이나 수거를 하지 않아도 된다는 특징이 있습니다.
참고 : GC는 벤더사마다 다양하게 구현하고 있고, 기본적인 동작 원리나 GC 튜닝 등 추가적으로 공부하면 좋은 내용이 많기에 따로 정리하겠습니다.
Native Method Interface & Library
JVM이 Java가 아닌 다른 언어들로 작성된 라이브러리들을 호출할 수 있도록 하는 프레임워크
참고자료
http://ksoong.org/troubleshooting/intro/jvm-generation.html
https://www.geeksforgeeks.org/jvm-works-jvm-architecture/
'JAVA' 카테고리의 다른 글
[Java] Garbage Collection & Garbage Collector (0) | 2024.07.09 |
---|---|
자바 레벨에서의 동시성 (1) | 2024.01.19 |
DateTime 자동 저장 시 OS에 따른 Time Precision 문제 (0) | 2024.01.17 |
[Effective Java] equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2023.04.02 |
[Effective Java] equals는 일반 규약을 지켜 재정의하라 (0) | 2023.03.26 |