[Java] 자바 가상 머신(JVM: Java Virtual Machine)이란?
Intro
C++과 자바는 인기 있는 객체지향 프로그래밍 언어이다. 하지만 자바는 C++과는 다르게 운영체제에 독립적이라는 큰 차이가 있다.
즉, C++은 운영체제에 따라 새로운 프로그램을 작성해야 하는 반면에, 자바는 소스 코드를 한 번만 작성하면, 어떤 운영체제에서도 코드를 수정할 필요 없이 프로그램을 실행시킬 수 있다.
이렇게 운영체제에 독립적일 수 있도록 도와주는 것이 자바 가상 머신(JVM: Java Virtual Machine)이다.
JVM이란? (자바 가상 머신)
JVM은 자바 프로그램을 실행시키는 도구로 자바 언어로 작성한 코드들은 JVM이 해석하게 되는 별도의 프로그램이다.
먼저, 프로그램이 실행되기 위해서는 CPU, 메모리, I/O 장치 등 컴퓨터 자원을 프로그램이 할당받아야 한다. 이러한 작업을 운영체제가 하게 되는데, 문제점은 운영체제마다 자원을 할당하는 방식이 다르다는 것이다. 이는 프로그래밍 언어가 운영체제에 독립적이지 못하는 이유이기도 하다.
하지만, 자바가 운영체제로부터 독립적일 수 있는 이유는 JVM을 통해 여러 운영체제와 소통이 가능하기 때문이다. 즉, JVM이 자바 프로그램과 운영체제 사이에서 통역하는 역할을 하는 것이다.
JVM의 구조
먼저 자바로 소스 코드를 작성하고 실행하면 이루어지는 순서는 다음과 같다.
- 자바 소스 코드를 실행하면 컴파일러가 컴파일을 진행하여 자바 바이트 코드 파일로 변환된다.
- JVM은 운영 체제로부터 소스 코드 실행에 필요한 메모리를 할당받는다.(Runtime Data Area)
- 클래스 로더(Class Loader)가 바이트 코드 파일을 런타임 데이터 영역에 적재시킨다.
- 적재가 끝나면 실행 엔진이 런타임 데이터 영역에 적재된 바이트 코드를 실행시킨다.
- 실행 엔진은 인터프리터(Interpreter) 또는 JIT Compiler를 통해 기계어로 번역하고 실행한다.
인터프리터 : 코드를 한 줄씩 번역하여 실행
JIT Compiler : 코드 전체를 기계어로 번역하고 실행
실행 엔진은 기본적으로 인터프리터 방식으로 실행시키다가 자주 실행되는 바이트 코드가 존재하면, 해당 바이트 코드를 JIT Compiler로 넘기게 된다.
즉, 인터프리터가 먼저 실행이 되고, JIT 컴파일러가 동작하면 모든 실행을 한 번에 동작하게 된다.
런타임 데이터 영역(Runtime Data Area)
JVM 메모리 구조
소스 코드를 실행하면 JVM은 실행에 필요한 메모리를 할당받고, 런타임 데이터 영역에 적재한다.
런타임 데이터 영역이 이러한 정보를 담는 메모리 영역이다.
런타임 데이터 영역에는 메서드 영역, 힙 영역, 스택 영역, PC 레지스터, 네이티브 메서드 스택이 있다.
Stack Area
스택은 자주 쓰이는 자료 구조 중 하나로 데이터를 저장하는 방식이다. 스택은 마치 데이터를 탑처럼 쌓듯이 올려놓아, 꺼낼 때 맨 위에 있는 데이터부터 꺼내는 방식이다. 이를 후입선출(LIFO) 방식이라 부르기도 한다.
JVM에서 스택은 메서드가 호출되면 해당 메서드를 위한 공간인 Method Frame이 생성된다.
메서드 내부에서 사용하는 참조 변수, 매개변수, 지역변수, … 등의 데이터를 임시로 저장한다.
이러한 Method Frame이 스택에 호출되는 순서대로 쌓이게 되는데, Method 동작이 완료되면 역순으로 빠져나간다.
Heap Area
JVM에는 단 하나의 힙 영역이 존재한다. JVM이 작동되면 자동으로 생성되고, 이곳에 객체, 인스턴스 변수, 배열 등이 저장된다.
Car car = new Car();
다음과 같이 객체를 생성할 때 new Car();을 통해 생성자를 호출하면서 힙 영역에 인스턴스가 생성된다.
인스턴스가 생성된 위치의 주소 값을 car 참조 변수에게 할당해 주며, car는 스택 영역에 선언된 변수이다.
즉, 자바에서 객체를 다룬다는 것은 스택 영역에 저장되어 있는 참조 변수를 통해 힙 영역에 존재하는 객체를 다루는 것이다. 따라서 객체의 주소를 담고 있는 car는 스택에, 실제 객체의 값이 저장되는 곳은 힙이다.
PC Register
PC 레지스터는 스레드가 생성될 때마다 생기는 공간으로 스레드가 어떤 소스 코드를 실행할지를 담는다. JVM은 스택에서 Operand를 추출하여 메모리 공간에 저장하는 Stacks-Base 방식으로 작동하는데 저장되는 메모리 공간이 바로 PC 레지스터이다.
Method Area
메서드 영역은 JVM이 프로그램을 실행할 때 클래스가 사용되면 해당 클래스의 인스턴스 변수, 메서드 등을 저장하는 공간이다. 즉, 객체가 생성된 후, 메서드를 실행하면, 해당 클래스에 대한 정보를 메서드 영역에 저장하는 것이다.
Native Method Stack
네이티브 메서드 스택은 자바 외의 언어에서 제공하는 메서드 정보를 저장하는 공간이다. JNI(Java Native Interface)를 통해 표준에 가까운 방식으로 구현한다.
가비지 컬렉션(Garbage Collection)
Garbage Collection이란 자바에서 메모리를 자동으로 관리하는 프로세스 중 하나로 프로그램에서 더 이상 사용하지 않는 객체를 찾아 제거하여 메모리를 확보하는 컬렉션이다.
Car car = new Car();
car.setName("현대 자동차");
car = null; // 가비지 발생
car = new Car();
car.setName("기아 자동차");
다음과 같은 코드가 있을 때, car = null; 코드로 인해 car이 가리키던 인스턴스와 참조 변수가 끊어지게 된다.
이와 같이 프로그램이 실행 중일 때, 인스턴스를 참조하고 있지 않는다면, 더 이상 car가 가리키던 인스턴스는 존재할 이유가 사라진다.
가비지 컬렉터(Garbage Collector)는 이와 같이 참조되지 않고 있는 객체와 변수들을 검색하여 메모리에서 제거하여 메모리를 확보한다.
가비지 컬렉션의 동작 방식
먼저, 객체와 인스턴스 변수, 배열 등의 데이터는 힙 영역에 저장되는데,
JVM의 힙 영역은 객체는 대부분 일회성이며, 메모리에 남아 있는 기간 등의 정보로 설계되어 있다.
따라서, 객체가 얼마나 존재하느냐에 따라 힙 영역 안에서도 Young, Old 영역으로 나누고 있다.
새롭게 생성된 객체는 Young 영역에 할당되며 이 영역에서는 객체가 생성되었다 사라지는 것을 반복한다. 이 영역에서 활동하는 가비지 컬렉터를 Minor GC라 부른다.
Old 영역에서는 Young 영역에서 오랜 기간 상태를 유지하는 객체들이 복사되는 곳으로 Young 역역보다 크게 할당된다. 이곳에서는 가비지가 적게 발생하며 이 영역에서 활동하는 가비지 컬렉터를 Major GC라 부른다.
Young 영역과 Old 영역은 서로 다른 메모리 구조로 되어 있기 때문에 동작 방식은 서로 다르지만 다음과 같은 단계를 따른다.
Stop The World
가비지 컬렉션을 실행시키기 위해 JVM이 프로그램의 실행을 멈추는 작업이다. 가비지 컬렉션이 실행될 때 가비지 컬렉션을 실행하는 스레드를 제외한 모든 스레드들의 작업이 중단된다.
Mark and Sweep
Mark는 사용하는 메모리와 사용하지 않는 메모리를 식별하며, Sweep는 사용하지 않는 메모리를 해제하는 작업이다.
먼저, Stop The World를 통해 모든 작업을 중단시키고 가비지 컬렉션은 객체와 변수들을 탐색한다. 이후 사용하는 메모리와 사용하지 않는 메모리를 식별하고 사용하지 않는 메모리를 제거한다.