JVM의 동작 방식
JVM의 동작 방식
JVM의 역할은 자바 애플리케이션을 클래스 로더를 통해 읽어 자바 API와 함께 실행하는 것이다.
- JVM은 OS로부터 메모리를 할당 받는다.
- 자바 컴파일러가 자바 소스코드(.java)를 자바 바이트 코드(.class)로 컴파일 한다.
- 클래스 로더는 동적 로딩을 통해 필요한 클래스들을 로딩 및 링크해서 Runtime Data Area에 올린다.
- Runtime Data Area에 로딩 된 바이트 코드는 Execution Engine을 통해 해석된다.
- Execution Engine에 의해 가비지 컬렉터의 작동과 스레드 동기화가 이루어진다.
JVM의 구조
클래스 로더
클래스 로더는 JVM 내로 클래스 파일을 동적으로 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈이다. 로드된 바이트 코드들을 엮어서 JVM의 메모리 영역인 Runtime Data Areas에 배치한다.
클래스를 메모리에 올리는 로딩 기능은 한번에 메모리에 올리는 것이 아닌, 어플리케이션에서 필요한 경우 동적으로 메모리에 적재하게 된다.
- Loading : 클래스 파일을 가져와서 JVM의 메모리에 로드한다.
- Linking : 클래스 파일을 사용하기 위해 검증하는 과정이다.
- Verifying : 읽어들인 클래스가 JVM 명세에 명시된 대로 구성되어 있는지 검사한다.
- preparing : 클래스가 필요로 하는 메모리를 할당한다.
- Resolving : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
- Initialization : 클래스 변수들을 적절한 값으로 초기화한다.
런타임 데이터 영역
JVM의 메모리 영역으로 자바 어플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역이다.
-
Method Area
모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. 클래스 정보와 메서드와 필드, static 변수, 메서드 바이트 코드 등을 보관한다. 모든 스레드가 공유하는 영역이다.
-
Runtime Constant Pool
각 클래스/인터페이스 마다 별도의 constant pool 테이블이 존재하는데, 클래스 생성할때 참조해야할 정보들을 상수로 가지고 있는 영역이다.
Constant Pool을 통해 해당 메소드나 필드의 실제 메모리 상 주소를 찾아 참조한다.
상수 자료형을 저장하여 참조하고 중복을 막는 역할
Java7 부터는 Method Area에서 Heap 으로 변경되었다고 한다.
-
Heap Area
new 연산자로 생성되는 클래스와 인스턴스 변수, 배열 타입 등 Reference Type이 저장되는 곳이다.
힙 영역은 모든 스레드가 공유된다.
런타임 시 동적으로 할당하여 사용하는 영역이다.
가비지 컬렉션에 대상이 되는 공간이다.
-
Stack Area
스택 영역은 int, long, boolean 등 기본 자료형을 생성할 때 저장하는 공간으로, 임시적으로 사용되는 변수나 정보들이 저장되는 영역이다.
메서드 호출 시마다 각각의 스택 프레임(그 메서드만을 위한 공간) 이 생성되고 메서드 안에서 사용되는 값들을 저장하고, 호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장한다. 메서드 수행이 끝나면 프레임별로 삭제된다.
각 스레드마다 하나씩 존재한다.
-
PC Register
현재 수행중인 JVM의 명령어 주소를 저장하는 공간. 스레드가 어떤 부분을 어떤 명령어로 수행할지를 저장하는 공간이다.
-
Native Method Stack
Java가 아닌 다른 언어로 작성된 코드를 위한 공간이다. JNI를 통해 호출하는 C/C++등의 코드를 수행하기 위한 공간이다.
Execution Engine
실행 엔진은 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다.
바이트 코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경해준다.
이 수행 과정에서 인터프리터와 JIT 컴파일러 두 가지 방식을 혼합하여 바이트 코드를 실행한다.
인터프리터
바이트 코드 명령어를 하나씩 읽어서 해석하고 실행한다.
JVM 안에서 바이트코드는 기본적으로 인터프리터 방식으로 동작한다.
같은 메서드라도 여러번 호출이 된다면 매번 해석하고 수행해야 되서 전체적인 속도가 느리다.
JIT 컴파일러
인터프리터 단점을 보완하기 위해 도입된 방식으로 반복되는 코드를 발견해 바이트 코드 전체를 컴파일해 Native Code로 변경하고 캐싱해 두었다가 네이티브 코드로 직접 실행하는 방식이다.
컴파일된 네이티브 코드를 실행하는 것이기 때문에 전체적인 실행 속도는 인터프리팅 방식보다 빠르다. 하지만 바이트코드를 네이티브 코드로 변환하는데 비용이 소요되므로 인터프리터 방식을 사용하다 일정 기준이 넘어가면 JIT 컴파일 방식으로 명령어 실행하는 식으로 진행한다.
가비지 컬렉터
Heap 메모리 영역에서 더는 사용하지 않는 메모리를 자동으로 회수해준다.
개발자가 따로 메모리를 관리하지 않아도 되므로, 손쉽게 개발할 수 있다.
GC가 실행되는 시간은 정해져 있지 않고, Full GC가 발생하는 경우에는 GC를 제외한 모든 스레드가 중지된다.
알고리즘은 다른 포스팅을 통해서 더 자세히 알아보자.
JNI (Java Native Interface
자바가 다른 언어로 만들어진 어플리케이션과 상호 작용할 수 있는 인터페이스를 제공하는 프로그램이다.
JVM이 Native Method를 적재하고 수행할 수 있도록 도와준다.
Native Method Library
C, C++로 작성된 라이브러리를 칭한다.
헤더가 필요하면 JNI는 이 라이브러리를 로딩해 실행한다.