JVM?
JVM은 자바 가상 머신이다. 단순히 *자바를 돌리는 프로그램*으로 이해하면 된다.
자바로 작성된 모든 프로그램은 JVM 위에서만 실행될 수 있으므로, 자바 프로그램을 실행하기 위해서는 반드시 자바 가상 머신이 설치되어 있어야 한다.
JVM은 JRE(Java Runtime Enviroment)에 포함되어 있다.
따라서 JRE 혹은 JDK를 설치했다면 JVM은 설치되어있다고 생각하면 된다.
- JVM의 장점
Java는 OS에 종속적이지 않다는 특징을 가진다.
C언어의 경우, 각 OS 환경마다 독자적인 컴파일러를 가지고 있기 때문에 다른 환경이 된다면 컴파일 결과가 달라진다.
따라서 컴파일러가 운영체제마다 의존적이었던 문제를 해결하고자 Java의 JVM이 등장했다.
Java 언어로 작성된 소스파일은 운영체제 레벨에서 직접 실행되는 것이 아닌, *JVM을 거쳐서 운영체제와 상호 작용을 하게 된다.*
이전에 OS마다 컴파일러가 존재했던 상황과 비교해보면 개발자는 정말 편해졌다.
이전까지는 다양한 OS 환경에 맞춰진 컴파일러가 이해할 수 있도록 직접적인 코드 변경이 필요했지만, 이제는 그럴 필요가 없다.
단순히 각 운영체제에 JVM만 설치되어있다면 모든 운영체제에서 자바 프로그램이 동작할 수 있다.
- JVM의 단점
장점이 있으면 단점도 있기 마련이다.
자바 프로그램은 이 외의 프로그램과 비교했을 때 JVM이라는 한 단계를 더 거쳐야해서 결국 실행속도는 상대적으로 느리다.
다시 말해 컴파일을 총 두 번 하기 때문에 실행 속도가 느리다.
이를 보완하기 위해 내부적으로 JIT 컴파일러를 사용해 성능 향상을 가져왔지만, 결국 C언어의 실행속도를 잡지는 못했다.
- JVM의 동작 방식
JVM의 역할은
- 자바 애플리케이션(.class)을 클래스 로더를 통해 읽어오는 것으로 시작해 -> Class Loader
- 필요한 정보를 메모리에 적재하고 -> Runtime Data Area
- 실행하는 것이다. -> Execution Engine
JVM의 구조
JVM의 구조를 좀 더 상세히 살퍼보자.
JVM은 아래와 같이 구성된다.
- 클래스 로더 (Class Loader)
- 실행 엔진 (Execution Engine)
- 인터프리터 (Interpreter)
- JIT 컴파일러 (Just-in-Time)
- 가비지 콜렉터 (GC)
- 런타임 데이터 영역 (Runtime Data Area)
- 메소드 영역
- 힙 영역
- 스택 영역
- PC Register
- 네이티브 메소드 스택 영역
- 클래스 로더 (Class Loader)
클래스 로더는 JVM 내부로 클래스 파일(.class)을 동적으로 로드하고, 로드된 바이트 코드들을 엮어서 *JVM의 메모리 영역인 Runtime Data Area에 배치한다.*
- 실행 엔진 (Execution Engine)
실행 엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드(.class)를 명령어 단위로 읽어서 실행한다.
자바의 바이트 코드(.class)는 기계가 바로 이해할 수 있는 완전한 기계어는 아니고, JVM이 이해할 수 있는 중간 레벨의 컴파일 코드이다. 따라서 실행 엔진은 이와 같은 중간 레벨의 바이트 코드를 JVM 내부에서 기계가 실행할 수 있는 형태로 바꿔준다.
이렇게 완전한 기계어로 변경해주는 과정은 실행 엔진의 *인터프리터*와 *JIT 컴파일러* 두 가지 방식을 혼합하여 이루어진다.
- 인터프리터
인터프리터는 바이트 코드 명령어를 하나씩 읽고 해석하고 바로 실행하는 방식이다.
JVM 안에서는 기본적으로 인터프리터 방식으로 동작한다! 다만 같은 메소드가 반복돼서 호출된다면 매번 해석하고 실행해 속도가 많이 느릴 것이다. 이러한 문제를 해결하기 위해 JIT 컴파일러가 등장했다.
- JIT 컴파일러
JIT 컴파일러는 반복되는 코드(메소드)를 발견해, 해당 바이트 코드 전체를 컴파일해 Native Code로 변경한 뒤 캐싱한다. 이 후 해당 메소드를 다시 호출했을 때는 인터프리터를 거치지 않고 캐싱해둔 Native Code로 직접 실행하는 방식이다. (Native Code란 C, C++, 혹은 어셈블리어를 뜻한다.)
이미 컴파일된 네이티브 코드를 실행하는 것이기 때문에 실행 속도 자체는 인터프리팅 방식보다 빠르다.
하지만 바이트코드를 Native Code로 변환하고 캐싱하는 데에도 비용이 소요되므로, JVM은 모든 코드를 JIT 컴파일러 방식을 통해 실행하지 않고 인터프리터 방식을 사용하다가 *일정 기준이 넘어간다면 JIT 컴파일러 방식을 사용한다.*
- 가비지 컬렉터
JVM의 실행 엔진에는 가비지 컬렉터가 있다.
가비지 컬렉터는, 런타임 데이터 영역 중 Heap 영역에서 더 이상 사용하지 않는 메모리를 자동으로 회수해준다.
C언어의 경우 free()를 호출해 개발자가 직접 메모리를 해제해줘야 되지만, Java는 이 가비지 컬렉터를 이용해 자동으로 메모리를 최적화 시켜준다.
일반적으로 자동으로 실행되지만, GC가 실행되는 시간은 정해져 있지 않다.
* 가비지 컬렉터에 대한 자세한 이야기는 별도의 포스트로 작성 예정입니다.
- 런타임 데이터 영역 (Runtime Data Area)
런타임 데이터 영역은 JVM의 메모리 영역으로, 자바 프로그램을 실행할 때 사용되는 데이터들을 적재하는 공간이다.
런타임 데이터 영역은 아래 그림과 같이 5가지로 구분할 수 있다.
이 때 *메소드 영역, 힙 영역은 모든 쓰레드가 공유하는 영역*이고, 나머지 스택 영역, PC Register, Native Method Stack은 각 쓰레드 마다 생성되는 개별 영역이다.
따라서 메소드 영역과 힙 영역의 데이터를 안전하게 다루기 위해서 적절한 동기화 매커니즘을 적용할 수 있어야한다.
- 메소드(static) 영역
JVM 시작 시 생성되며 프로그램이 종료될 때 까지 유지된다.
static 변수(클래스 변수), 생성자와 메소드 등 클래스와 인터페이스에 관한 정보가 저장된다.
일반 인스턴스 변수는 메소드 영역에 저장되지 않는다.
-> 메소드 영역에는 *static 필드와 클래스 구조만*을 갖고 있다고 말할 수 있다.
Runtime Constant Pool은 메소드 영역에 존재하는 별도의 관리 영역이다.
각 클래스 or 인터페이스마다 별도의 Constant Pool을 가지며, 클래스를 생성할 때 참조해야하는 정보들을 상수로 가지고 있는 영역이다.
- 힙 영역
힙 영역은 런타임 시 동적으로 할당하여 사용하는 영역이다.
해당 영역에는 new 연산자를 통해 생성되는 인스턴스와 배열과 같은 *Reference Type*이 저장되는 곳이다.
힙 영역에 생성된 객체와 배열은 Reference Type이므로 JVM의 스택 영역의 변수에서 참조된다.
즉, 힙의 참조 주소는 스택이 갖고 있고 해당 스택의 참조 변수를 통해서만 힙 영역에 있는 인스턴스를 핸들링할 수 있다.
만약, 참조하는 변수나 필드가 없다면 의미 없는 객체가 되기 때문에 JVM은 가비지 컬렉터를 통해 객체를 제거한다.
- 스택 영역
스택 영역은 메소드 내에서 정의하는 기본 자료형에 해당되는 지역변수의 데이터 값이 저장되는 공간이다.
메소드가 호출될 때 스택 영역에는 *스택 프레임(해당 메소드만을 위한 공간)*이 생기고, 그 안에 메소드에 대한 정보가 저장된다.
메소드가 호출될 때 스택 영역의 메모리에 할당되고 메소드가 종료되면 사라진다.
[스택 프레임]
스택 프레임에 쌓이는 데이터는 메소드의 매개변수, 지역변수, 리턴값, 연산 시 결과값 등이 있다.
이 때, 저장되는 데이터의 타입에 따라 저장되는 방식이 다르다.
- 원시 타입 변수 : 직접 값을 갖는다.
- 참조 변수 : 힙 영역이나 메소드 영역의 주소값을 갖는다.
- PC Register
PC 레지스터는 쓰레드가 시작될 때 생성되며, 멀티 쓰레드 환경에서 한 쓰레드가 작업을 하다가 다른 쓰레드로 CPU 자원을 넘겨주고 다시 받았을 때, 이어서 작업을 하기 위해 현재 실행중인 JVM 명령(Instruction)의 주소를 기록한다.
만약 쓰레드가 자바 메소드를 수행하고 있었으면 JVM 명령의 주소를 저장하겠지만, 다른 언어의 메소드를 수행하고 있다면 undefined 샹태가 된다.
왜냐하면 자바에서는 이 두 경우를 따로 처리하기 때문이다.
이 부분이 다음에 이어서 나올 Native Method Stack 영역이다.
- Native Method Stack 영역
네이티브 메소드 스택은 자바 코드가 컴파일되어 생성되는 바이트 코드가 아닌, 실제 기계가 바로 실행할 수 있는 *기계어로 작성된 프로그램을 실행시키는 영역이다.*
위에서 언급했던 JIT 컴파일러에 의해 변환된 Native Code 또한 여기서 실행된다.
출처 :
'Java' 카테고리의 다른 글
[Java] 스레드(Thread) 총정리 (0) | 2024.01.19 |
---|---|
[Java] 가비지 컬렉션(Garbage Collection) (1) | 2024.01.12 |
[Java] 제네릭(Generics) (0) | 2024.01.02 |
[Java] 컬렉션 프레임워크 (0) | 2024.01.02 |
[Java] Stream API (1) | 2023.12.21 |