JVM 개요

누구나 JVM 이라는 용어를 한 번쯤 들어봤을 것이고, 전공 과목으로 자바를 배웠다면 더욱이 친숙할 단어이다.

얼핏 기억나는 사람들은 어림짐작 가능할 것이다. JVM 의 핵심 역할은 다음과 같다.

- JVM 기반 프로그램이 플랫폼, 운영체제에 국한되지 않고 실행될 수 있도록 해줌
- JVM 기반 프로그램의 메모리를 관리하고 최적화해줌

JVM 은 코드를 실행하고, 해당 코드에 대한 런타임 환경을 제공하는 프로그램에 대한 사양

한 줄로 정리하자면, JVM 은 자바 기반 애플리케이션을 클래스 로더를 통해 읽어들이고, 자바 API 와 함께 실행하는 역할을 한다. Java 프로그램과 OS 중간에 껴서 중개자 역할을 함으로써 기기 및 OS의 제한없이 프로그램의 재사용성을 증대시킨다.

그런데 이들이 어떻게 구성되어있고, 이들이 어떻게 엮여서 실행되는지를 알아야 할 필요가 있다.
따라서 이번 포스팅에선 이와 관련한 내용들을 알아보고자 한다.


JVM 의 기본 구성

JVM 기반 프로그램이 실행되는 과정을 살펴보기 전에, 먼저 JVM 을 구성하는 요소들 각각에 대해 알아보자. 하나씩 알아가다보면 끼워맞춰지는 그 순간이 있을 것이다.

자바 컴파일러 (JAVAC)

자바 소스코드(.java)를 바이트 코드(.class)로 변환, 컴파일해주는 역할을 수행한다.


클래스 로더

우리가 자바 프로그램을 짠다고 하면, 그 결과물은 .java 파일이 된다. 이러한 .java 파일은 위에서 소개한 Javac (컴파일러) 에 의해 .class 바이트 코드로 컴파일되게 된다.

JVM 은 런타임 시에 최초로 클래스(바이트 코드)를 참조할 때, 해당 클래스(바이트 코드)를 로드하고 메모리 영역 (아래에서 소개하는 Runtime Data Areas) 에 배치시킨다. 이 동적 로딩을 담당하는 부분이 바로 클래스 로더이다.


Execution Engine

위에서 소개한 클래스 로더에 의해 메모리 영역 (Runtime Data Areas) 에 적재된 바이트 코드(.class)들을 하나의 명령 단위로 읽어서 컴퓨터가 이해할 수 있는 기계어로 번역(해석)하고 명령을 수행한다. 이 과정을 수행하는 방식으로 Interpreter 방식과 JIT (Just-In-Time) 방식이 존재한다.


Runtime Data Areas

JVM 이 운영체제 위에서 실행되면서 적재된 프로그램이 요구하는 만큼 메모리를 할당받으면, 이 메모리를 아래처럼 5가지 영역으로 나누어 관리하게 된다.

5가지 영역으로 나누어짐 : PC 레지스터, JVM 스택, 네이티브 메소드 스택, 힙, 메소드 영역

  1. PC 레지스터 : 현재 쓰레드가 다음으로 어떤 명령을 수행해야 할지 기록하는 부분 (JVM 명령의 주소를 가짐)
    → 우리가 알고 있는 그 Program Counter 이다! (알고있나?)

  2. JVM 스택 : 지역변수, 메소드의 매개변수, 메소드 정보, 임시 데이터 등을 저장하는 부분

  3. 네이티브 메소드 스택 : 자바 이외의 다른 언어에서 작성된 네이티브 코드를 수행하기 위한 메모리 영역. 컴파일되어 생성되는 바이트 코드를 해석해서 실행하는 것이 아닌, 실제 당장 수행할 수 있는 기계어로 작성된 네이티브 프로그램을 실행시키는 영역이다.

  4. : 런타임 시 동적으로 할당되는 데이터가 저장되는 영역 (new 등을 통해 생성한 객체, 배열 등)
    → 힙에 할당된 녀석들은 GC 의 대상이기 때문에 JVM 성능 이슈에서 가장 많이 언급되는 영역이기도 하다.

  5. 메소드 영역 : JVM 이 시작될 때 생성되고, JVM 이 읽은 각각의 클래스와 인터페이스에 대한 멤버 변수 (필드) 및 생성자를 포함한 메소드 코드, static & final 변수, 메소드의 바이트 코드 등을 보관하는 부분이다. 한 번 로드된 후 메모리에 항상 상주하고 있는 영역이기 때문에, 모든 쓰레드가 공유 가능하다.


Garbage Collection

이에 관해서는 따로 포스팅해둔 적이 있다. 해당 포스팅을 참고하자.

GC 과정 요약

참조되지 않은 객체 탐색 후 삭제 → 삭제된 객체의 메모리 반환 → 힙 메모리 재사용


JVM 프로그램 실행 흐름

구성요소는 이 정도이고, 이들이 어떻게 엮여 JVM 위에서 프로그램이 실행되는 지에 대한 과정을 살펴보자.

  1. 우선 JVM 기반 프로그램이 실행되면, JVM 은 OS 로부터 해당 프로그램이 요구하는 만큼 메모리를 할당받는다. JVM 은 할당받은 메모리를 용도에 따라 여러 영역 (Runtime Data Areas
    ) 으로 나누어 관리
    하게 된다.

  2. Java 컴파일러 (Javac) 가 자바 소스코드를 읽고, 이를 바이트 코드로 변환시킨다(.class)

  3. 변환된 바이트 코드 파일들을 클래스 로더를 통해 JVM 메모리 영역으로 로딩한다.

  4. 로딩된 바이트 코드 파일들은 Execution Engine 을 통해 해석된다.

  5. 해석된 바이트 코드는 메모리 영역에 배치되어 실질적인 동작 수행이 이루어진다. 이 동작 수행 과정 속에서 JVM 은 필요에 따라 쓰레드 동기화, GC 같은 메모리 관리 작업을 수행한다.


구성 요소 각각에 대해 살펴본 뒤 실행 흐름을 살펴보면, 어느정도 JVM 의 동작 흐름을 이해할 수 있을 것이다. JVM 기반 언어를 통해 프로그램을 개발하는 사람들이라면 이에 대해 꼭 알아둘 필요가 있다.

profile
어려울수록 기본에 미치고 열광하라

0개의 댓글